-
Notifications
You must be signed in to change notification settings - Fork 9
Syntax Reference
Axel's semantics are very close to Haskell's – in fact, if you remove Axel's metaprogramming capabilities, you can think of Axel as just an alternate syntax for Haskell. In this article, we'll cover these syntactic differences.
Axel's syntax is modeled off of the Lisp family of programming languages. The main conceptual difference from Haskell is that Axel requires parentheses around all function calls. Fogr example, foo $ bar (baz quux)
translates into Axel as (foo (bar (baz quux)))
. This extends to statemgents as well (e.g. foo a = a + 1
becomes (= foo (a) (+ a 1))
). This may seem arbitrary and weird at first, but this style allows for incredible flexibility via macros. Because Axel programs are represented by trees of nested parentheses, they are very easy to manipulate and reshape to your heart's content. (For an introduction to Axel's metaprogramming system, please see Introduction to Macros.)
Because all Axel programs are written in this parenthesized prefix notation, we need to translate Haskell's basic syntactic constructs (let
blocks, case
expressions, import
statements, etc.) into this parenthesized style. Also, because Axel's syntax is much simpler than Haskell's, many more characters are allowed in identifier names. We'll cover all this and more below, so keep reading!
Syntax: -- some comment
Haskell equivalent: Comments are not currently kept in the transpiled output, but we're working on it (https://github.com/axellang/axel/issues/25)!
Examples:
(foo bar) -- Should we be using `foo` here?
-- TRANSPILES INTO
(foo bar)
Notes:
Axel's single-line comment syntax is the same as Haskell's (there is not yet any dedicated syntax for multi-line comments).
Syntax: (raw "some Haskell statement")
or (raw "some Haskell expression")
Haskell equivalent: The Haskell statement or expression provided.
Examples:
(raw "foo :: Foo")
(= foo () (bar (raw "@Baz"))) -- Transpiles into: foo = bar @Baz
-- TRANSPILES INTO
foo :: Foo
foo = bar @Baz
Notes:
Axel doesn't currently support all of Haskell's syntax natively (but we're working on it! https://github.com/axellang/axel/issues/2). So, if you run into a limitation of Axel's syntax, or just want to quickly use a Haskell snippet inside an Axel program, you can use Axel's provided escape-hatch: raw
.
For example, if you want to use Template Haskell inside an Axel program – e.g. makeLenses ''Foo
– just wrap the statement with a call to raw
and everything will work as you expect – e.g. (raw "makeLenses ''Foo")
. Or, let's say you want to use a type application, which doesn't yet have dedicated Axel syntax – e.g. foo @Bar baz
. Use raw
, like so: (foo (raw "@Bar") baz)
.
This even means that you can gradually convert a Haskell file to Axel, by wrapping each statement in its own call to raw
and then converting them as you find convenient!
Syntax: (foo a b c)
Haskell equivalent: foo a b c
Examples:
(putStrLn (<> "Hello, " "world!"))
-- TRANSPILES INTO:
putStrLn (<> "Hello, " "world!")
Notes:
Note that infix operators are also applied in prefix form by default, so (+ 1)
transpiles into ((+) 1)
rather than (+ 1)
.
Syntax: {arg1 function arg2}
Haskell equivalent: arg1 ``function`` arg2
, but function
can be any valid Axel form (instead of just a symbol, as in Haskell).
Examples:
{1 + 2}
{'a' elem "abc"}
{'a' (\ (x y) (elem x y)) "abc"}
-- TRANSPILES INTO:
1 + 2
'a' `elem` "abc"
(\x y -> elem x y) 'a' "abc"
Notes:
{a op b}
is converted by the parser into (applyInfix a op b)
.
applyInfix
is a macro from the Axel Prelude which transpiles its input into the form (op a b)
.
Notes:
Identifiers (e.g. function names) can contain any character other than (
, )
, {
, }
, [
, ]
, and "
and they must not start with '
, `
, or ~
.
Examples of valid Axel identifiers are fooBar
, a->b
, <+>
, and isn't
. However, 'foo
and ~bar
(for example) would both be considered invalid identifiers.
Any Haskell function can be called from Axel as expected. However, since Axel is less restrictive than Haskell in this regard, an Axel identifier (e.g. a->b
) might not be valid in Haskell. Thus, we only recommend calling an Axel function from Haskell code if its name is valid in both languages, since Axel has to specially escape characters that Haskell disallows. Currently, the way in which it does so should be considered an implementation detail; thus, escaped function names may change without notice between Axel versions.
Syntax: #\x
Haskell equivalent: 'x'
Examples:
#\A
#\\
#\'
-- TRANSPILES INTO:
'A'
'\\'
'\''
Notes:
This character literal syntax is borrowed from Lisps like Common Lisp and Racket. Axel can't use Haskell's character literal syntax because the quote character ('
) is reserved for Axel's metaprogramming facilities (for example, 'a'
is interpreted in Axel as a quotation of a symbol named a'
).
Notes:
This is the same as in Haskell.
Notes:
This is the same as in Haskell.
Syntax: "some string \""
Haskell equivalent: "some string \""
Notes:
This is the same as in Haskell.
Syntax: unit
or Unit
Haskell equivalent: ()
Examples:
(:: main (() (IO Unit)))
(= main () (pure unit))
-- TRANSPILES INTO:
main :: IO ()
main = pure ()
Notes:
Since (
and )
are reserved characters in Axel, ()
is not a valid identifier name. Thus, the synonyms Unit
and unit
are provided for use in Axel. Although it is customary to use Unit
when you would otherwise use ()
as a type, and unit
when you would otherwise use ()
as a value, they both transpile to the same Haskell token and thus are interchangeable.
Syntax: (list listItem1 listItem2 ... listItemN)
or [listItem1 listItem2 ... listItemN]
Haskell equivalent: [listItem1, listItem2, ..., listItemN]
Examples:
[(putStr "Hello, ") (putStr "world!")]
(length (list))
-- TRANSPILES INTO:
[putStr "Hello, ", putStr "world!"]
length []
Notes:
The use of the [...]
literal syntax is preferred (the list
special form is provided for use in macros).
Syntax: List
Haskell equivalent: []
Examples:
(:: foo (() (List Int)))
-- TRANSPILES INTO:
foo :: [Int]
Notes:
List
is provided as the type-level equivalent of Haskell's []
(since [
and ]
are reserved characters in Axel).
Syntax:
(= functionName (arg1 arg2 ... argN)
functionBody
optionalWhereBinding1
optionalWhereBinding2
...
optionalWhereBindingN)
Haskell equivalent:
functionName arg1 arg2 ... argN = functionBody
where
optionalWhereBinding1
optionalWhereBinding2
...
optionalWhereBindingN
Examples:
(= foo (message) (go message)
(:: go (() (-> String (IO Unit))))
(= go () putStrLn))
-- TRANSPILES INTO:
foo message = go message
where
go :: String -> IO ()
go = putStrLn
Notes:
Whenever your definition has an associated type signature, use def
instead of =
. (The documentation for def
contains a more detailed discussion.) However, for function definitions that aren't top-level statements (e.g. where
bindings), =
can be useful as a lightweight alternative to def
.
Syntax: (:: functionName functionConstraints functionType)
Haskell equivalent: functionName :: functionConstraints => functionType
Examples:
(:: foo () Int)
-- TRANSPILES INTO:
foo :: () => Int
Notes:
Whenever your type signature has an associated definition, use def
instead of ::
and =
. (The documentation for def
contains a more detailed discussion.)
Syntax:
(def name (constraints type)
((clause1arg1 clause1arg2 ... clause1argN) body1 optionalWhereBindings1)
((clause2arg1 clause2arg2 ... clause2argN) body2 optionalWhereBindings2)
...
((clauseNarg1 clauseNarg2 ... clauseNargN) bodyN optionalWhereBindingsN))
Haskell equivalent:
name :: constraints => type
name clause1arg1 clause1arg2 ... clause1argN = body1 where optionalWhereBindings1
name clause2arg1 clause2arg2 ... clause2argN = body2 where optionalWhereBindings2
...
name clauseNarg1 clauseNarg2 ... clauseNargN = bodyN where optionalWhereBindingsN
Examples:
(def foo (() {(Maybe Int) -> Int})
((Just x) x)
(Nothing 0))
-- TRANSPILES INTO:
foo :: Maybe Int -> Int
foo (Just x) = x
foo Nothing = 0
Notes:
Because top-level statements are only allowed to rely on those before them in the presence of macros, type signatures should come after function definitions if those function definitions contain calls to macros. Otherwise, if the type signature came before, the environment available to the macro definition would contain a type signature with no associated definition (since the definition is still being compiled!).
However, placing type signatures after function definitions is non-standard Haskell (and thus Axel) style, and so the def
macro from the Axel Prelude handles type signature placement for us – while still keeping our function definitions Haskell-like.
Syntax:
(class (constraint1 constraint2 ... constraintN) classHead
classSigOrDef1
classSigOrDef2
...
classSigOrDefN)
Haskell equivalent:
class (constraint1, constraint2, ..., constraintN) => classHead where
classSigOrDef1
classSigOrDef2
...
classSigOrDefN
Examples:
(class ((Eq a)) (Foo a)
(:: foo (() {a -> {a -> Bool}}))
(= foo () ==)
(:: bar (() {b -> b}))
(= bar (x) x))
-- TRANSPILES INTO:
class (Eq a) => Foo a where
foo :: a -> a -> Bool
foo = (==)
bar :: a -> a
Syntax:
(instance (constraint1 constraint2 ... constraintN) instanceHead
instanceDef1
instanceDef2
...
instanceDefN)
Haskell equivalent:
instance (constraint1, constraint2, ..., constraintN) => instanceHead where
instanceDef1
instanceDef2
...
instanceDefN
Examples:
(instance ((Eq a)) (Foo a)
(= foo (_ _) True)
(= bar (x) x))
-- TRANSPILES INTO:
instance (Eq a) => Foo a where
foo = (==)
bar x = x
Syntax:
(data dataType
constructor1
constructor2
...
constructorN)
Haskell equivalent:
data dataType = constructor1 | constructor2 | ... | constructorN
Examples:
(data (Maybe a)
(Just a)
Nothing)
-- TRANSPILES INTO:
data Maybe a
= Just a
| Nothing
Syntax: (recordType (field1 type1) (field2 type2) ... (fieldN typeN))
Haskell equivalent: { field1 :: type1, field2 :: type2, ..., fieldN :: typeN }
Examples:
(data Foo
(Foo (recordType (foo Int)
(bar String))))
-- TRANSPILES INTO:
data Foo = Foo { foo :: Int, bar :: String }
Notes:
For recordType
's value-level equivalent, see record
.
Syntax: (newtype name wrappedType)
OR (newtype (name arg1 arg2 ... argN) wrappedType)
Haskell equivalent: newtype name = name wrappedType
OR newtype name arg1 arg2 ... argN = name wrappedType
Examples:
(newtype EmailAddress String)
(newtype (Id a) a)
-- TRANSPILES INTO:
newtype EmailAddress = EmailAddress String
newtype Id a = Id a
Notes:
Unlike in Haskell, the "right hand side" of the declaration doesn't need to contain the newtype name again (e.g. EmailAddress
and Id
are automatically added in the above transpilation examples).
Syntax: (type typeHead wrappedType)
Haskell equivalent: type typeHead = wrappedType
Examples:
(type EmailAddress String)
(type (Id a) a)
-- TRANSPILES INTO:
type EmailAddress = String
type Id a = a
Syntax: (module moduleIdentifier)
Haskell equivalent: module moduleIdentifier where
Examples:
(module Foo.Bar)
-- TRANSPILES INTO:
module Foo.Bar where
Notes:
Explicit export lists are not yet implemented in Axel.
Unlike in Haskell, Axel requires every module to have an explicit module declaration.
Syntax: (pragma "some pragma")
Haskell equivalent: {-# some pragma #-}
Examples:
(pragma "OPTIONS_GHC \"-fno-warn-incomplete-patterns\"")
(pragma "LANGUAGE GADTs")
-- TRANSPILES INTO:
{-# OPTIONS_GHC "-fno-warn-incomplete-patterns" #-}
{-# LANGUAGE GADTs #-}
Syntax: (import moduleName all)
OR (import moduleName (importItem1 importItem2 ... importItemN))
, where each importItem
is either an identifier or (containerType subitem1 subitem2 ... subitemN)
Transpiled into: import moduleName
OR import moduleName (importItem1, importItem2, ..., importItemN)
, where each importItem
is either the identifier or containerType(subitem1, subitem2, ..., subitemN)
Examples:
(import Foo all)
(import Bar (foo bar (Bar baz quux)))
-- TRANSPILED INTO:
import Foo
import Bar (foo, bar, Bar(baz, quux))
Notes:
Axel doesn't currently support aliasing unqualified imports (e.g. import Foo as Bar
).
Syntax: (importq moduleName alias all)
OR (importq moduleName alias (importItem1 importItem2 ... importItemN))
, where each importItem
is either an identifier or (containerType subitem1 subitem2 ... subitemN)
Transpiled into: import qualified moduleName as alias
OR import qualified moduleName as alias (importItem1, importItem2, ..., importItemN)
, where each importItem
is either the identifier or containerType(subitem1, subitem2, ..., subitemN)
Examples:
(importq Foo Baz all)
(importq Bar Quux (foo bar (Bar baz quux)))
-- TRANSPILED INTO:
import qualified Foo as Baz
import qualified Bar as Quux (foo, bar, Bar(baz, quux))
Notes:
Axel doesn't currently have an equivalent for the Haskell construction import qualified Foo (...)
(i.e. there is an implicit as Foo
).
Syntax:
(case expr
(pattern1 body1)
(pattern2 body2)
...
(patternN bodyN))
Haskell equivalent:
case expr of
pattern1 -> body1
pattern2 -> body2
...
patternN -> bodyN
Examples:
(case (Just 1)
((Just a) {a + 1})
(Nothing 0))
-- TRANSPILES INTO:
case Just 1 of
Just a -> a + 1
Nothing -> 0
Syntax: (\ (arg1 arg2 ... argN) body)
Haskell equivalent: \arg1 arg2 ... argN -> body
Examples:
(\ (x y) {x + y})
-- TRANSPILES INTO:
\x y -> x + y
Syntax:
(\case
(pattern1 body1)
(pattern2 body2)
...
(patternN bodyN))
Haskell equivalent:
\<autogenerated> ->
case <autogenerated> of
pattern1 -> body1
pattern2 -> body2
...
patternN -> bodyN
Examples:
(\case
((Just x) {x + 1})
(Nothing 0))
-- TRANSPILES INTO:
\<autogenerated> -> case <autogenerated> of
Just x -> x + 1
Nothing -> 0
Notes:
\case
is the Axel equivalent of the the LambdaCase
GHC extension.
\case
is a macro from the Axel Prelude. (This is a great example of macros extending the syntactic flexibility of the language! In Haskell, a compiler extension was required to implement such functionality, whereas it becomes a very simple macro in Axel.)
Syntax: (if condition ifTrue ifFalse)
Haskell equivalent: if condition then ifTrue else ifFalse
Examples:
(if {"yes" == "yes"}
(putStrLn "Cool!")
(error "Impossible!"))
-- TRANSPILES INTO:
if "yes" == "yes"
then putStrLn "Cool!"
else error "Impossible!"
Syntax:
(let ((pattern1 value1)
(pattern2 value2)
...
(patternN valueN))
body)
Haskell equivalent:
let pattern1 = value1
pattern2 = value2
...
patternN = valueN
in body
Examples:
(let (((Just x) (fromJust maybeResult))
(y {x + 1}))
(putStrLn (show y)))
-- TRANSPILES INTO:
let Just x = fromJust maybeResult
y = x + 1
in putStrLn (show y)
Syntax: (record (field1 value1) (field2 value2) ... (fieldN valueN))
Haskell equivalent: { field1 = value1, field2 = value2, ..., fieldN = valueN }
Examples:
(Foo (record (foo 1) (bar "test")))
-- TRANSPILES INTO:
Foo { foo = 1, bar = "test" }
Notes:
Haskell's record update syntax (e.g. quux { foo = 1 }
) is not yet supported in Axel.
For record
's type-level equivalent, see recordType
.
Syntax: (do' clause1 clause2 ... clauseN)
, where each clause
is (<- pattern val)
, (let ((pattern1 val1) (pattern2 val2) ... (patternN valN)))
, or val
Haskell equivalent:
do clause1
clause2
...
clauseN
where each clause
is either:
pat <- val
-
let pattern1 = val1 pattern2 = val2 ... patternN = valN
val
Examples:
(do' (<- input getLine)
(let ((reversed (reverse input))
(output (map toUpper reversed))))
(putStrLn output))
-- TRANSPILES INTO:
do input <- getLine
let reversed = reverse input
output = map toUpper reversed
putStrLn output
Notes:
do'
is a macro from the Axel Prelude. (This is a great example of macros extending the syntactic flexibility of the language!)
Support for clauses of the form {pat <- val}
is in progress (https://github.com/axellang/axel/issues/19).
For a tutorial on how to use the below items in real-world programs, as well as more information as to how they work, please see Introduction to Macros.
Notes:
Wherever a function call is allowed, a macro name may be used instead of the function. In this case, the macro will be called at compile-time, with its arguments being the Abstract Syntax Tree (AST) representations of the Axel forms that were passed to it.
When a macro is called, the collection of statements before the one that includes the macro call must remain valid on their own. (For an example, see the documentation for def
and the rationale behind using it instead of ::
and =
).
Macros are expanded top-down.
See =macro
for more information.
Syntax: 'form
OR (quote form)
Haskell equivalent: Abstract Syntax Tree (AST) representation of form
.
Examples:
'1
'-123.45
'#\a
'"test"
'foo
'(1 2 3)
''a
-- TRANSPILES INTO:
AST.LiteralInt <metadata> 1
AST.LiteralFloat <metadata> (-123.45)
AST.LiteralChar <metadata> 'a'
AST.LiteralString <metadata> "test"
AST.Symbol <metadata> "foo"
AST.SExpression <metadata> [AST.LiteralInt <metadata> 1, AST.LiteralInt <metadata> 2, AST.LiteralInt <metadata> 3]
AST.SExpression <metadata> [AST.Symbol <metadata> "quote", <quoted metadata>, AST.LiteralChar
Notes:
'
behaves similarly to the quote operator in other Lisps. Quotations are evaluated like any other form (i.e. bottom-up, at evaluation time).
'form
and (quote form)
are equivalent.
The metadata included in the AST data structures tells Axel where the expressions in question were originally located (i.e. file name, line, and column).
Due to the inclusion of source metadata, it's not safe to use a quoted expression in a pattern-match. For example, (= foo ('symbol) body)
is virtually guaranteed to never be called, since the Eq
instance for AST.Expression
will take the source position of the quoted symbol
into consideration, which will not match the source position of whatever argument is passed into foo
(even if the symbol in question is named "symbol").
For an alternative to '
that can be used in pattern-matching, see the documentation for syntaxQuote
.
For documentation on the AST data structures themselves and how to manipulate them, see the Hackage documentation for (this is currently prevented by https://github.com/haskell/haddock/issues/900).Axel.Parse.AST
Syntax: `form
OR (quasiquote form)
Haskell equivalent: AST representation of form
, except after accounting for ~
and ~@
Examples:
`(foo ~bar ~@['baz1 baz2])
`~foo
-- TRANSPILES INTO:
AST.SExpression <metadata> [AST.Symbol <metadata> "foo", bar, AST.Symbol <metadata> "baz1", baz2]
foo
Notes:
`
behaves similarly to the quasiquote (or "backquote") operator in other Lisps. Quasiquotations are expanded into the corresponding calls to quote
by the parser. Nested backquotes are handled as in e.g. Common Lisp.
`form
and (quasiquote form)
are equivalent.
Quasiquotation is the same as quotation, except:
- Whenever
~form
is encountered, the value ofform
will be used instead of its AST representation - Whenever
(... ~@form ...)
is encountered, the elements insideform
(which must be either a list or a quoted s-expression) are inserted directly into the containing form
For documentation on the AST data structures themselves and how to manipulate them, see the Hackage documentation for Axel.Parse.AST
.
Syntax: ~form
OR (unquote form)
Notes:
~
behaves similarly to the unquote operator in other Lisps (it is the equivalent of ,
in e.g. Common Lisp).
~form
and (unquote form)
are equivalent.
The use of ~form
is only valid when inside a form passed to `
. It instructs the quasiquote expander to refrain from quoting form
, and instead insert its value literally into the result of the quasiquotation.
See the documentation for `
for more details.
Syntax: ~@form
OR (unquoteSplicing form)
Notes:
~@
behaves similarly to the splice-unquote operator in other Lisps (it is the equivalent of ,@
in e.g. Common Lisp).
~@form
and (unquoteSplicing form)
are equivalent.
The use of (... ~@form ...)
is only valid when inside an s-expression (which is part of a form passed to `
), and when form
evaluates to either a list or a quoted s-expression. It instructs the quasiquote expander to refrain from quoting form
, and instead insert the values of form
's elements literally into the result of its containing s-expression.
See the documentation for `
for more details.
Notes:
Axel.Parse.AST
is implicitly imported into every Axel file, aliased to AST
. See the Hackage documentation for Axel.Parse.AST
for details on how to use what's imported under the AST
namespace.
Syntax: (importm moduleIdentifier (macro1 macro2 ... macroN))
Examples:
(importm moduleIdentifier (macroFoo macroBar))
-- TRANSPILES INTO:
<moduleIdentifier.macroFoo and moduleIdentifier.macroBar are now in-scope>
Notes:
Macros cannot be imported with the traditional import
syntax, so importm
is a special form provided for this purpose. All macros are (currently) exported by default from their defining modules.
Axel automatically imports all macros (and functions) from the Axel Prelude, so imports of the form (importm Axel (...))
are likely redundant.
Syntax: (=macro name ([arg1 arg2 ... argN]) body optionalWhereBindings)
Examples:
(=macro applyInfix ([x op y])
(pure [`(~op ~x ~y)]))
-- TRANSPILES INTO:
<A macro such that e.g. `(applyInfix x + y)` is converted into `(+ x y)`>
Notes:
Macros are currently provided a single array as their only argument.
When a macro is defined, the only statements (e.g. imports, function definitions) it knows about are those which come before the call to =macro
in the file.
Macro type signatures are autogenerated by Axel, such that all macros have the type [AST.Expression <metadata>] -> IO [AST.Expression <metadata>]
. (This means that calls to IO
are allowed during a macro expansion, but it's recommended to keep side-effectful actions to a minimum, as always.)
When defining a macro, try not to discard the metadata information from received expressions. This is very important for the Axel compiler to be able to point the user of your macro to the right place in case of errors.
For the =macro
equivalent to def
, see defmacro
. Prefer =macro
only for very lightweight macro definitions.
Syntax:
(defmacro name
(pattern1 body1 optionalWhereBindings1)
(pattern2 body2 optionalWhereBindings2)
...
(patternN bodyN optionalWhereBindingsN))
Examples:
(defmacro def
({name : {typeSig : cases}}
(pure
(snoc (map (\ (x) `(= ~name ~@x))
cases)
`(:: ~name ~@typeSig)))))
-- TRANSPILES INTO:
<The `def` macro described in this reference>
Notes:
Each pattern
refers to an array of AST representations of the macro's arguments. defmacro
behaves similarly to def
, but without the type signature specification.
When a macro is defined, the only statements (e.g. imports, function definitions) it knows about are those which come before the call to defmacro
in the file.
Macro type signatures are autogenerated by Axel, such that all macros have the type [AST.Expression <metadata>] -> IO [AST.Expression <metadata>]
. (This means that calls to IO
are allowed during a macro expansion, but it's recommended to keep side-effectful actions to a minimum, as always.)
When defining a macro, try not to discard the metadata information from received expressions. This is very important for the Axel compiler to be able to point the user of your macro to the right place in case of errors.
Use defmacro
instead of =macro
where e.g. you'd otherwise need to include multiple calls to =macro
to pattern match on the argument array differently.
Syntax: (syntaxQuote form)
Examples:
(syntaxQuote foo)
-- TRANSPILES INTO:
(AST.SExpression _ "foo")
Notes:
syntaxQuote
is a variation of quote
that is safe for use in pattern-matches. It replaces what would normally be the source metadata of the quoted form with _
, such that you can say e.g. (= foo ((syntaxQuote symbol)) body)
and match on (foo 'symbol)
(whereas (= foo ('symbol) body)
would actually not be called in this case). See the documentation for quote
for more detail.
syntaxQuote
doesn't currently have special syntax like quote
does.