lfe_guide(7)

πŸ“¦ version 2.2.0, in alpine 3.24, nixos 26.05, nixpkgs unstable

πŸ’– NAME πŸ’–

okay bestie, welcome to lfe_guide(7) β€” straight outta the Miscellaneous Information Manual β€” aka the Lisp Flavoured Erlang User Guide. this is the whole literal-syntax tour and it's giving main character energy fr fr ✨


πŸ¦‹ SYNOPSIS πŸ¦‹

quick heads up before we start: {{ ... }} is how we show optional syntax. that's it, that's the synopsis πŸ’…


πŸ”₯ LITERALS AND SPECIAL SYNTACTIC RULES πŸ”₯

Integers

numbers can be written in so many forms and number bases, it's honestly iconic 🌈

  • Regular decimal notation:
1234 -123 0
  • Binary notation:
#b0 #b10101 #b-1100
  • Binary notation (alternative form):
#*0 #*10101 #*-1100
  • Octal notation:
#o377 #o-111
  • Explicitly decimal notation:
#d1234 #d-123 #d0
  • Hexadecimal notation:
#xc0ffe #x-01
  • Notation with explicit base (up to 36):
#2r1010 #8r377 #36rhelloworld
  • Character notation (the value is the Unicode code point of the character):
#\a #\$ #\Γ€
  • Character notation with the value in hexadecimal:
#\x1f42d;

in all these forms, the case of the indicating letter is not significant, i.e. #b1010 and #B1010 are identical, as are #16rf00 and #16Rf00. lowkey liberating 🫢

similarly, case is not significant for digits beyond 9 (i.e. a, b, c, ... for number bases larger than 10), e.g. #xabcd is the same as #xABCD and can even be mixed in the same number, e.g. #36rHelloWorld is valid and the same number as #36Rhelloworld and #36rHELLOWORLD. chaos but make it consistent πŸ’€

the character notation using hexadecimal code representation (#\x....;) is basically the same thing as the regular hexadecimal notation #x... except that it conveys to the reader that a character is intended and that it does a sanity check on the value (e.g. negative numbers and value outside the Unicode range are not permitted). it's the responsible one of the group chat πŸŽ€

Floating point numbers

there is only one type of floating point numbers and the literals are written in the usual way, e.g. these are all valid floating point numbers:

1.0 +1.0 -1.0 1.0e10 1.111e-10

the one thing to watch out for is that you cannot omit the part before or after the decimal point if it is zero. e.g. the following are not valid forms: 100. or .125. sorry bestie, no shortcuts there 😭

Strings

there are two forms of strings: list strings and binary strings. two queens standing side by side πŸ‘‘

List Strings

list strings are just lists of integers (where the values have to be from a certain set of numbers that are considered valid characters) but they have their own syntax for literals (which will also be used for integer lists as an output representation if the list contents looks like it is meant to be a string): β€œany text between double quotes where " and other special characters like \n can be escaped”.

as a special case you can also write out the character number in the form \xHHH; (where β€œHHH” is an integer in hexadecimal notation), e.g. "\x61;\x62;\x63;" is a complicated way of writing "abc". this can be convenient when writing Unicode letters not easily typeable or viewable with regular fonts. e.g. "Cat: \\x1f639;" might be easier to type (and view on output devices without a Unicode font) then typing the actual Unicode letter. ngl that's a vibe 🌸

Binary Strings

binary strings are just like list strings but they are represented differently in the virtual machine. the simple syntax is #"...", e.g.

#"This is a binary string \n with some \"escaped\" and quoted (\\x1f639;) characters"

you can also use the general format for creating binaries (#B(...), described below), e.g. #B("a"), #"a", and #B(97) are all the same binary string. three names, one icon πŸ’…

Character Escaping

certain control characters can be more readably included by using their escaped name:

  • \b β€” Backspace
  • \t β€” Tab
  • \n β€” Newline
  • \v β€” Vertical tab
  • \f β€” Form Feed
  • \r β€” Carriage Return
  • \e β€” Escape
  • \s β€” Space
  • \d β€” Delete

alternatively you can also use the hexadecimal character encoding, e.g. "a\nb" and "a\x0a;b" are the same string. same girl, different outfit ✨

Triple-quoted Strings

LFE's triple-quote strings are modelled on those in Erlang so we will use their description of them.

strings, both list strings and binary strings, can also be written as triple-quoted strings, which can be indented over multiple lines to follow the indentation of the surrounding code. they are also verbatim, that is, they do not allow escape sequences, and thereby do not need double quote characters to be escaped. no cap, zero escaping stress 🧚

example, with verbatim double quote characters:

"""
  Line "1"
  Line "2"
  """

that is equivalent to the normal single quoted string (which also allows newlines):

"Line \"1\"
Line \"2\""

the opening and the closing lines have the delimiters: the """ characters. the lines between them are the content lines. the newline on the opening line is not regarded as string content, nor is the newline on the last content line.

the indentation is defined by the white space character sequence preceding the delimiter on the closing line. that character sequence is stripped from all content lines. there can only be white space before the delimiter on the closing line, or else it is regarded as a content line. rules are rules πŸŽ€

the opening line is not allowed to have any characters other than white space after the delimiter, and all content lines must start with the defined indentation character sequence, otherwise the string has a syntax error. so behave πŸ’€

here is a larger example:

"""
      First line starting with two spaces
    Not escaped: "\t \r \xFF" and """

    """

that corresponds to the normal string:

"  First line starting with two spaces
Not escaped: \"\\t \\r \\xFF\" and \"\"\"
"

binary strings can also be written as triple-quoted strings:

#"""
  Line "1"
  Line "2"
  """

which correspnds to the binary:

#"Line \"1\"\nLine \"2\""

Function References

the function references are the forms:

(function func-name arity)
(function mod-name func-name arity)

these forms evaluate to function references which can be used with funcall to call the functions.

there is also a special syntax:

#'func-name/arity
#'mod-name:func-name/arity

which is still supported but is deprecated. ⚠️ still works, still frowned upon, you've been warned bestie

Binaries

we have already seen binary strings, but the #B(...) syntax can be used to create binaries with any contents. unless the contents is a simple integer you need to annotate it with a type and/or size.

example invocations are that show the various annotations:

> #B(42 (42 (size 16)) (42 (size 32)))
#B(42 0 42 0 0 0 42)
> #B(-42 111 (-42 (size 16)) 111 (-42 (size 32)))
#B(-42 111 (-42 (size 16)) 111 (-42 (size 32)))
> #B((42 (size 32) big-endian) (42 (size 32) little-endian))
#B(0 0 0 42 42 0 0 0)
> #B((1.23 float) (1.23 (size 32) float) (1.23 (size 64) float))
#B(63 243 174 20 122 225 71 174 63 157 112 164 63 243 174 20
   122 225 71 174)
> #B((#"a" binary) (#"b" binary))
#"ab"

learn more about β€œsegments” of binary data e.g. in β€œLearn You Some Erlang” http://learnyousomeerlang.com/starting-out-for-real#bit-syntax. go read it, it ate πŸ”₯

Lists

lists are formed either as ( ... ) or [ ... ] where the optional elements of the list are separated by some form or whitespace. for example:

()
(the empty list)
(foo bar baz)
(foo
 bar
 baz)

Tuples

tuples are written as #(value1 value2 ...). the empty tuple #() is also valid. tiny but valid 🫢

Maps

maps are written as #M(key1 value1 key2 value2 ...) the empty map is also valid and written as #M(). πŸ’œ

Structs

structs are written as #S(struct-name key1 value1 key2 value2 ...).

⚠️ note that structs cannot be created with the literal syntax, the (struct mod-name ...) form must be used. literal syntax said no πŸ’…

Symbols

things that cannot be parsed as any of the above are usually considered as a symbol.

simple examples are foo, Foo, foo-bar, :foo. but also somewhat surprisingly 123foo and 1.23e4extra (but note that illegal digits don't make a number a symbol when using the explicit number base notation, e.g. #b10foo gives an error). plot twist 🌈

symbol names can contain a surprising breadth or characters, basically all of the latin-1 character set without control character, whitespace, the various brackets, double quotes and semicolon.

of these, only |, \', ', ,, and # may not be the first character of the symbol's name (but they are allowed as subsequent letters).

i.e. these are all legal symbols: foo, foo (yes, twice πŸ’€), Β΅#, Β±1, 451Β°F.

symbols can be explicitly constructed by wrapping their name in vertical bars, e.g. |foo|, |symbol name with spaces|. in this case the name can contain any character of in the range from 0 to 255 (or even none, i.e. || is a valid symbol). the vertical bar in the symbol name needs to be escaped: |symbol with a vertical bar \| in its name| (similarly you will obviously have to escape the escape character as well). escape-ception 😭

Comments

comments come in two forms: line comments and block comments.

line comments start with a semicolon (;) and finish with the end of the line.

block comments are written as #| comment text |# where the comment text may span multiple lines but my not contain another block comment, i.e. it may not contain the character sequence #|. no nesting, please and thank you πŸŽ€

Evaluation While Reading

#.(... some expression ...). e.g. #.(+ 1 1) will evaluate the (+ 1 1) while it reads the expression and then be effectively 2. read AND math at the same time, multitasking queen πŸ‘‘


…and that's literally just the literals section, we're so back 🫢 more coming ✨

πŸ’– Supported forms πŸ’–

okay so this is the big one, bestie β€” the forms LFE actually supports. no cap, we're listing them all. grab a snack πŸ“.

🧚 Core forms 🧚

these are the core forms, the main-character lineup. literally every one:

              (quote e)
              (cons head tail)
              (car e)
              (cdr e)
              (list e ... )
              (tuple e ... )
              (tref tuple index)
              (tset tuple index val)
              (binary seg ... )
              (map key val ...)
              (map-size map) (msiz m)
              (map-get map key) (mref m k)
              (map-set map key val ...) (mset m k v ...)
              (map-update map key val ...) (mupd m k v ...)
              (map-remove map key ...) (mrem m k k ...)
              (andalso ... )
              (orelse ... )
              (lambda (arg ...) ...)
              (match-lambda
                ((arg ... ) {{(when e ...)}} ...)           - Matches clauses
                ... )
              (function func-name arity)                    - Function reference
              (function mod-name func-name arity)
              (let ((pat {{(when e ...)}} e)
                    ...)
                ... )
              (let-function ((name lambda|match-lambda)     - Local functions
                             ... )
                ... )
              (letrec-function ((name lambda|match-lambda)  - Local functions
                                ... )
                ... )
              (let-macro ((name lambda-match-lambda)        - Local macros
                          ...)
                ...)
              (progn ... )
              (prog1 ...)
              (prog2 ...)
              (if test true-expr {{false-expr}})
              (case e
                (pat {{(when e ...)}} ...)
                ... ))
              (cond (test body ...)
                    ...
                    ((?= pat expr) ...)
                    ...
                    (else ...))
              (maybe
                ...
                {{(else ((pat) {{(when e ...)}} ... )       - Else clauses
                       ...)}})
              (receive
                (pat {{(when e ...)}} ... )
                ...
                (after timeout ... ))
              (catch ... )
              (try
                e
                {{(case ((pat {{(when e ...)}} ... )
                        ... ))}}
                {{(catch
                   ((tuple type value stacktrace)|_ {{(when e ...)}}
                                          - Must be tuple of length 3 or just _!
                    ... )
                   ... )}}
                {{(after ... )}})
              (funcall func arg ... )
              (call mod func arg ... )    - Call to Mod:Func(Arg, ... )

              (define-record name fields)
              (record name field val ...)
              (is-record record name)
              (record-index name field)
              (record-field record name field)
              (record-update record name field val ...)

              (define-struct fields)
              (struct mod-name field val ...)
              (is-struct struct)
              (is-struct struct name)
              (struct-field struct name field)
              (struct-update struct name field val ...)

              (define-module name meta-data attributes)
              (extend-module meta-data attributes)

              (define-function name meta-data lambda|match-lambda)
              (define-macro name meta-data lambda|match-lambda)

              (define-type type definition)
              (define-opaque-type type definition)
              (define-function-spec func spec)

that's the core forms list, periodt. no missing entries, no vibes-based edits πŸ’….

πŸŽ€ Basic macro forms πŸŽ€

these are the basic macro forms, exactly as the page lists them. the => bits are literally written like that, bestie β€” don't come for me:

              (: mod func arg ... ) =>
                      (call 'mod 'func arg ... )
              (mod:func arg ... ) =>
                      (call 'mod 'func arg ... )
              (? {{timeout {{default}} }})
              (++ ... )
              (-- ... )
              (list* ... )
              (let* (... ) ... )
              (flet ((name (arg ...) {{doc-string}} ...)
                     ...)
                ...)
              (flet* (...) ... )
              (fletrec ((name (arg ...) {{doc-string}} ...)
                        ...)
                ...)
              (fun func arity)                              - Expand to lambdas
              (fun mod func arity)
              (lc (qual ...) expr)
              (list-comp (qual ...) expr)
              (bc (qual ...) bitstringexpr)
              (binary-comp (qual ...) bitstringexpr)
              (ets-ms ...)
              (trace-ms ...)

slay, that's the whole basic macro squad. ✨

🌸 Common Lisp inspired macros 🌸

and here's the Common Lisp inspired macros. yes, defmacro has three shapes. it's giving range, honestly:

              (defun name (arg ...) {{doc-string}} ...)
              (defun name
                {{doc-string}}
                ((argpat ...) ...)
                ...)
              (defmacro name (arg ...) {{doc-string}} ...)
              (defmacro name arg {{doc-string}} ...)
              (defmacro name
                {{doc-string}}
                ((argpat ...) ...)
                ...)
              (defsyntax name
                (pat exp)
                ...)
              (macrolet ((name (arg ...) {{doc-string}} ...)
                         ...)
                ...)
              (syntaxlet ((name (pat exp) ...)
                          ...)
                ...)
              (defmodule name ...)
              (defrecord name ...)
              (defstruct ...)

obsessed with how defun and defmacro can take either a plain arg list or a match-style clause situation. iconic. 🫢

πŸ¦‹ Patterns πŸ¦‹

patterns are written as normal data expressions β€” symbols are variables, and you use quote to match explicit values. binaries and tuples have their own special syntax, iykyk:

              {ok,X}                  -> (tuple 'ok x)
              error                   -> 'error
              {yes,[X|Xs]}            -> (tuple 'yes (cons x xs))
              <<34,U:16,F/float>>     -> (binary 34 (u (size 16)) (f float))
              [P|Ps]=All              -> (= (cons p ps) all)

repeated variables are supported in patterns, and there's an automatic comparison of values. so yes, the same variable can show up more than once and LFE will check that the values line up. βœ…

_ as the β€œdon’t care” variable is supported. this means that the symbol _ β€” which is a perfectly valid symbol β€” can never be bound through pattern matching. it's the designated shrug emoji of patterns. πŸ€·β€β™€οΈ

aliases are defined with the (= pattern1 pattern2) pattern. as in Erlang patterns, they can be used anywhere in a pattern. cute, flexible, powerful. πŸ’…

CAVEAT the lint pass of the compiler checks for aliases and if they are possible to match. if not, an error is flagged. this is not the best way. instead there should be a warning and the offending clause removed, but later passes of the compiler can’t handle this yet. so yeah β€” that's the current vibe, not a glow-up.

🌈 Guards 🌈

wherever a pattern occurs (in let, case, receive, lc, etc.) it can be followed by an optional guard which has the form (when test ...). guard tests are the same as in vanilla Erlang, and they can contain the following guard expressions:

              (quote e)
              (cons gexpr gexpr)
              (car gexpr)
              (cdr gexpr)
              (list gexpr ...)
              (tuple gexpr ...)
              (tref gexpr gexpr)
              (binary ...)
              (map ...)
              (msiz ...) (map-size ...)
              (mref ...) (map-get ...)
              (mset ...) (map-set ...)
              (mupd ...) (map-update ...)
              (record ...)                - Also the macro versions
              (is-record ...)
              (record-index ...)
              (record-field ...)
              (is-struct ...)
              (struct-field ...)
              (type-test e)               - Type tests
              (guard-bif ...)             - Guard BIFs, arithmetic,
                                            boolean and comparison operators

an empty guard, (when), always succeeds as there is no test which fails. this simplifies writing macros which handle guards. tiny icon behavior. πŸŽ€

πŸ’œ Comments in Function Definitions πŸ’œ

inside functions defined with defun, LFE permits optional comment strings in the Common Lisp style after the argument list. so we can have:

              (defun max (x y)
                "The max function."
                (if (>= x y) x y))

optional comments are also allowed in match style functions after the function name and before the clauses:

              (defun max
                "The max function."
                ((x y) (when (>= x y)) x)
                ((x y) y))

this is also possible in a similar style in local functions defined by flet and fletrec:

              (defun foo (x y)
                "The max function."
                (flet ((m (a b)
                         "Local comment."
                         (if (>= a b) a b)))
                  (m x y)))

not me loving how you can just drop a little doc-string moment wherever. it's giving self-documenting code, fr fr. πŸ“

🧚 Variable Binding and Scoping 🧚

variables are lexically scoped and bound by lambda, match-lambda and let forms. all variables which are bound within these forms shadow variables bound outside, but other variables occurring in the bodies of these forms will be imported from the surrounding environments. no variables are exported out of the form. so for example the following function:

              (defun foo (x y z)
                (let ((x (zip y)))
                  (zap x z))
                (zop x y))

the variable y in the call (zip y) comes from the function arguments. however, the x bound in the let will shadow the x from the arguments, so in the call (zap x z) the x is bound in the let while the z comes from the function arguments. in the final (zop x y) both x and y come from the function arguments as the let does not export x. no leakage, bestie. the let keeps its x to itself. πŸ’…

πŸ‘‘ Function Binding and Scoping πŸ‘‘

functions are lexically scoped and bound by the top-level defun and by the macros flet and fletrec. LFE is a Lisp-2 so functions and variables have separate namespaces, and when searching for a function both name and arity are used. this means that when calling a function which has been bound to a variable, using (funcall func-var arg ...) is required to call a lambda/match-lambda bound to a variable or used as a value. yes, the function namespace and variable namespace are living in separate group chats. 😌

unqualified functions shadow as stated above, which results in the following order within a module, outermost to innermost:

  • Predefined Erlang BIFs
  • Predefined LFE BIFs
  • Imports
  • Top-level defines
  • Flet/fletrec
  • Core forms, these can never be shadowed

this means that it is perfectly legal to shadow BIFs by imports, BIFs/imports by top-level functions and BIFs/imports/top-level by fletrecs. in this respect there is nothing special about BIFs, they just behave as predefined imported functions, a whopping big (import (from erlang ...)). EXCEPT that we know about guard BIFs and expression BIFs. if you want a private version of spawn then define it, there will be no warnings. go off, honestly. πŸš€

CAVEAT this does not hold for the supported core forms. these can be shadowed by imports or redefined, but the compiler will always use the core meaning and never an alternative. the compiler will warn when core forms are redefined but it is not an error. so like β€” you can try it, but the compiler is the main character here and it's not taking notes. πŸ’€

πŸ’– Module definition πŸ’–

okay bestie, so the basic forms for defining a module and extending its metadata and attributes are literally these:

(define-module name meta-data attributes)
(extend-module meta-data attributes)

The valid meta data is (type typedef ...), (opaque typedef ...), (spec function-spec ...) and (record record-def ...). Each one can take multiple definitions in one meta form. slay.

Attribute declarations have the syntax (attribute value-1 ...) where the attribute value is a list off the values in the declaration.

To simplify defining modules there is a predefined macro:

(defmodule name
  "This is the module documentation."
  (export (f 2) (g 1) ... )
  (export all)                          ;Export all functions
  (import (from mod (f1 2) (f2 1) ... )
          (rename mod ((g1 2) m-g1) ((g2 1) m-g2) ... ))
  (module-alias (really-long-module-name rlmn) ...)
  (attr-1 value-1 value-2)
  {meta meta-data ...)
  ... )

We can have multiple export and import attributes within a module declaration. The (export all) attribute is allowed together with other export attributes and overrides them. Other attributes which are not recognized by the compiler are allowed and are simply passed on to the module and can be accessed with the module_info/0-1 functions.

In the import attribute the (from mod (f1 2) ...) means that the call (f1 'everything 42) will be converted by the compiler to (mod:f1 'everything 42)) while the (rename mod ((g2 2) m-g1) ...) means that the call (m-g1 'everything 42) will be converted to (mod:g1 'everything 42). The rename form can be used as a compact way of indicating the imported function’s module. Note that when importing a module:

  • the compiler does no checking on that module at all
  • in the rename above the functions g1/2 and g2/1 aren’t automatically imported, only the β€œrenamed” functions.
  • we do not really see in the code that we are calling a function in another module

In the module-alias attribute the (really-long-module-name rlmn) declaration means that the call (lrmn:foo 'everything 42) will be converted by the compiler to (really-long-module-name:foo 'everything 42). This is often used to write short module names in the code when calling functions in modules with long names. It is in many ways better than using import as it does not hide that we are calling a function in another module. iconic behavior tbh.


πŸ¦‹ Macros πŸ¦‹

Macro calls are expanded in both body and patterns. This can be very useful to have both make and match macros, but be careful with names. fr fr.

A macro is a function of two arguments which is called with a list of the arguments to the macro call and the current macro environment. It can be either a lambda or a match-lambda. The basic forms for defining macros are:

(define-macro name meta-data lambda|match-lambda)
(let-macro ((name lambda|match-lambda)
  ...)

Macros are definitely NOT hygienic in any form. However, variable scoping and variable immutability remove most of the things that can cause unhygienic macros. It can be done but you are not going to do it by mistake. The only real issue is if you happen to be using a variable which has the same name as one which the macro generates, that can cause problems. The work around for this is to give variables created in the macro expansion really weird names like | - foo - | which no one in their right mind would use. πŸ’…

To simplify writing macros there are a number of predefined macros:

(defmacro name (arg ...) ...)
(defmacro name arg ...)
(defmacro name ((argpat ...) body) ...)

defmacro can be used for defining simple macros or sequences of matches depending on whether the arguments are a simple list of symbols or can be interpreted as a list of pattern/body pairs. In the second case when the argument is just a symbol it will be bound to the whole argument list. For example:

(defmacro double (a) `(+ ,a ,a))
(defmacro my-list args `(list ,@args))
(defmacro andalso
  ((list e) `,e)
  ((cons e es) `(if ,e (andalso ,@es) 'false))
  (() `'true))

The macro definitions in a macrolet obey the same rules as defmacro.

The macro functions created by defmacro and macrolet automatically add the second argument with the current macro environment with the name $ENV. This allows explicit expansion of macros inside the macro and also manipulation of the macro environment. No changes to the environment are exported outside the macro.

User defined macros shadow the predefined macros so it is possible to redefine the built-in macro definitions. However, see the caveat below!

Yes, we have the backquote. It is implemented as a macro so it is expanded at macro expansion time. no cap.

Local functions that are only available at compile time and can be called by macros are defined using eval-when-compile:

(defmacro foo (x)
  ...
  (foo-helper m n)
  ...)

(eval-when-compile
  (defun foo-helper (a b)
    ...)

  )

There can be many eval-when-compile forms. Functions defined within an eval-when-compile are mutually recursive but they can only call other local functions defined in an earlier eval-when-compile and macros defined earlier in the file. Functions defined in eval-when-compile which are called by macros can defined after the macro but must be defined before the macro is used.

Scheme’s syntax rules are an easy way to define macros where the body is just a simple expansion. They are implemented in the module scm and are supported with scm:define-syntax and scm:let-syntax and the equivalent scm:defsyntax and scm:syntaxlet. Note that the patterns are only the arguments to the macro call and do not contain the macro name. So using them we would get:

(scm:defsyntax andalso
  (() 'true)
  ((e) e)
  ((e . es) (case e ('true (andalso . es)) ('false 'false))))

There is an include file include/scm.lfe which defines macros so the names don’t have to be prefixed with scm:.

CAVEAT While it is perfectly legal to define a Core form as a macro these will silently be ignored by the compiler. πŸ’€

πŸŽ€ Comments in Macro Definitions πŸŽ€

Inside macros defined with defmacro LFE permits optional comment strings in the Common Lisp style after the argument list. So we can have:

(defmacro double (a)
  "Double macro."
  `(+ ,a ,a))

Optional comments are also allowed in match style macros after the macro name and before the clauses:

(defmacro my-list args
  "List of arguments."
  `(list ,@args))

(defmacro andalso
  "The andalso form."
  ((list e) `,e)
  ((cons e es) `(if ,e (andalso ,@es) 'false))
  (() `'true))

This is also possible in a similar style in local functions defined by macrolet:

(defun foo (x y)
  "The max function."
  (macrolet ((m (a b)
               "Poor macro definition."
               `(if (>= ,a ,b) ,a ,b)))
    (m x y)))

🌸 RECORDS 🌸

Okay so records are tuples with the record name as the first element and the rest of the fields in order, exactly like β€œnormal” Erlang records. As with Erlang records, the default-default value (yes, that phrase is real) is the atom undefined.

The basic forms for defining a record, creating one, accessing it, and updating it are:

(define-record name (field | (field) |
                     (field default-value) |
                     (field default-value type) ...))
(record name field value field value ...)
(is-record record name)
(record-index name field)
(record-field record name field)
(record-update record name field value field value ...)

Note that when you’re making or updating a record, the list of field/value pairs is a flat list. No nesting, bestie.

🚨 Deprecation alert: the old make-record form has been deprecated and is replaced by record, which better matches other constructors like tuple and map. It still exists but should not be used.

Let’s explain with a simple example. To define a record we do:

(define-record person
               ((name "")
                (address "" (string))
                (age)))

which defines a record person with the fields name (default value ""), address (default value "" and type (string)) and age. To make an instance of a person record we do:

(record person name "Robert" age 54)

The record form is also used to define a pattern.

We can get the value of the address field in a person record and set it by doing (the variable robert references a person record):

(record-field robert person address)
(record-update robert person address "my home" age 55)

Note that we must include the name of the record when accessing it, and there’s no need to quote the record and field names because these are always literal atoms.

To simplify defining and using records there’s a predefined macro:

(defrecord name
  (field) | field
  (field default-value)
  (field default-value type)
  ... )

This will create access macros for record creation and accessing and updating fields. The make-, match- and update- forms takes optional argument pairs field-name value to get non-default values. E.g. for

(defrecord person
  (name "")
  (address "" (string))
  (age))

the following will be generated:

(make-person {{field value}} ... )
(match-person {{field value}} ... )
(is-person r)
(fields-person)
(update-person r {{field value}} ... )
(person-name r)
(person-name)
(update-person-name r name)
(person-age r)
(person-age)
(update-person-age r age)
(person-address r)
(person-address)
(update-person-address r address)
  • (make-person name "Robert" age 54) β€” Will create a new person record with the name field set to β€œRobert”, the age field set to 54 and the address field set to the default "".
  • (match-person name name age 55) β€” Will match a person with age 55 and bind the variable name to the name field of the record. Can use any variable name here.
  • (is-person john) β€” Test if john is a person record.
  • (person-address john) β€” Return the address field of the person record john.
  • (person-address) β€” Return the index of the address field of a person record.
  • (update-person-address john "back street") β€” Updates the address field of the person record john to β€œback street”.
  • (update-person john age 35 address "front street") β€” In the person record john update the age field to 35 and the address field to β€œfront street”.
  • (fields-person) β€” Returns a list of fields for the record. This is useful for when using LFE with Mnesia, as the record field names don’t have to be provided manually in the create_table call.
  • (size-person) β€” Returns the size of the record tuple.

Note that the older now deprecated set- forms are still generated.

πŸ¦‹ STRUCTS πŸ¦‹

Structs in LFE are the same as Elixir structs and have been defined in the same way so to be truly compatible. This means that you can use structs defined in Elixir from LFE and structs defined in LFE from Elixir. πŸ’–

(define-struct (field | (field) |
                (field default-value) |
                (field default-value type) ...))
(struct name field value field value ...)
(is-struct struct)
(is-struct struct name)
(struct-field struct name field)
(struct-update struct name field value field value ...)

We’ll explain these forms with a simple example. To define a struct we do:

(define-struct ((name "")
                (address "" (string))
                (age)))

which defines a struct with the name of the current module with the fields name (default value ""), address (default value "" and type (string)) and age. To make an instance of struct we do:

(struct mod-name name "Robert" age 54)

The struct form is also used to define a pattern.

We can get the value of the address field in the struct and set it by doing (the variable robert references a struct):

(struct-field robert mod-name address)
(struct-update robert mod-name address "my home" age 55)

Note that a struct automatically gets the name of the module in which it is defined so that there can only be one struct defined in a module. This mirrors how structs are implemented in Elixir.

Note that we must include the name of the struct when accessing it, and there’s no need to quote the struct and field names because these are always literal atoms.

🧚 BINARIES/BITSTRINGS 🧚

A binary is

(binary seg ... )

where seg is

byte
string
(val integer | float | binary | bitstring | bytes | bits |
     utf8 | utf-8 | utf16 | utf-16 | utf32 | utf-32
     (size n) (unit n)
     big-endian | little-endian | native-endian
     big | little | native
     signed | unsigned)

val can also be a string, in which case the specifiers will be applied to every character in the string. As strings are just lists of integers, these are also valid here. In a binary constant all literal forms are allowed on input but they will always be written as bytes.

πŸ“ MAPS πŸ“

A map is created with:

(map key value ... )

To access maps there are the following forms:

  • (map-size map) β€” Return the size of a map.
  • (map-get map key) β€” Return the value associated with the key in the map.
  • (map-set map key val ... ) β€” Set the keys in the map to values. This form can be used to update the values of existing keys and to add new keys.
  • (map-update map key val ... ) β€” Update the keys in the map to values. Note that this form requires all the keys to already exist in the map.
  • (map-remove map key ... ) β€” Remove the keys in the map.

There are also alternate short forms msiz, mref, mset, mupd and mrem based on the Maclisp array reference forms. They take the same arguments as their longer alternatives. πŸ’…

πŸŽ€ CORE FORMS πŸŽ€

okay bestie, this is the part where LFE actually does stuff β€” the form-y forms, the control flow, the "this but only if that" of it all. buckle up πŸ’…


🌸 IF 🌸

The LFE if is giving classic if energy way more than the Erlang if does β€” like, it actually behaves the way your brain expects. 😌

For the test it lets you use any Erlang boolean expression, and that expression must return either true or false.

The false expression is optional β€” and if you don't include it, the if just returns false. no drama, no exception, it just says no. 🫢

Some examples:

(if (our-test) (it-was-true))

(if (our-test) (it-was-true) (it-was-false))

πŸ¦‹ COND πŸ¦‹

cond is lowkey if's cooler cousin. It's similar to if, and the tests in cond are Erlang tests in that they should return either true or false.

Here's the iconic part: if no test succeeds, then the cond does not generate an exception β€” it just returns false. no screaming, no stack trace, just a polite little false. ✨

There's also a simple catch-all "test" called else, which must last, and which you can use to handle the case when all the tests fail.

cond has been extended with the extra test (?= pat expr), which tests if the result of expr matches the pattern pat. If it does, it binds the variables in pat and you can use them in the cond test body expression. An optional guard is allowed here too. πŸ‘‘

An example:

(cond ((foo x) ...)
      ((?= (cons x xs) (when (is_atom x)) (bar y))
       (fubar xs (baz x)))
      ((?= (tuple 'ok x) (baz y))
       (zipit x))
      ...
      (else 'yay))

πŸ’œ MAYBE πŸ’œ

LFE has an Erlang-compatible maybe β€” it's got the same features as the Erlang maybe, with the ?= operator and else and everything. fr fr.

The expressions in the maybe block get evaluated sequentially:

(maybe
  expr-1
  expr-2
  ...
  expr-n)

If all the expressions succeed, then the maybe block returns the value of expr-n. πŸŽ€

Now the conditional match:

(?= pattern expr)

can short circuit all of that. If the match succeeds, then the variables in pattern become bound. The ?= match returns the value of the expressions. But if the match fails, then the rest of the expressions in the maybe block are skipped, and maybe returns the value of the expr. πŸ’€

The maybe block can be augmented with else clauses:

(maybe
  expr-1
  ...
  expr-n
  (else
    ((pattern-1) body-1)
    ...
    ((pattern-n) (when guard-test) body-n)
  ))

If a conditional match fails, then the value of the expression is matched against the patterns in the else clauses, and if one of them matches, its body gets evaluated. Guard tests are allowed. And if no pattern matches? you get an else-clause run-time error. 😭 so like, have an else clause. have it.

An example:

(maybe
  (foo g)
  (?= `#(ok ,a) (a g))
  (bar g)
  (?= `#(ok ,b) (b g))
  (+ a b)
  (else
    (('error) 1 #(got error))
    (('wrong) 2 #(got wrong))
  ))

The maybe body can include ?= forms, which behave in the same way as they do in the Erlang maybe. But here's the thing β€” as LFE cannot bind variables the same way as Erlang does, we allow let in the body to bind variables. Those variables are only local in the let body, so this body gets "lifted" up to the top-level maybe body, which means it can contain ?= forms as well. 🧚 that's the whole trick, iykyk.


🌈 LIST/BINARY COMPREHENSIONS 🌈

List/binary comprehensions are supported as macros. yes bestie, macros. πŸ’…

The syntax for list comprehensions is:

(lc (qual  ...) expr)
(list-comp (qual  ...) expr)

where that last expr is used to generate the elements of the list. πŸ“

The syntax for binary comprehensions is:

(bc (qual  ...) bitstringexpr )
(binary-comp (qual  ...) bitstringexpr)

where the final expr is a bitstring expression, and it's used to generate the elements of the binary.

The supported qualifiers, in both list comprehensions and binary comprehensions, are:

(<- pat {{guard}} list-expr)        - Extract elements from list
(<= bin-pat {{guard}} binary-expr)  - Extract elements from binary
expr                                - Normal boolean test

Some examples, because obviously:

(lc ((<- v (when (> v 5)) l1)
     (== (rem v 2) 0))
  v)

returns a list of all the even elements of the list l1 which are greater than 5. slay. ✨

(bc ((<= (binary (f float (size 32))) b1)
     (> f 10.0))
  (progn
    (: io fwrite "~p\n" (list f))
    (binary (f float (size 64)))))

returns a binary of floats of size 64 bits, which come from the binary b1, where they are of size 32 bits and larger than 10.0. The returned numbers are first printed. πŸ–¨οΈ cute.

This could also be written using a guard for the test:

(bc ((<= (binary (f float (size 32))) (when (> f 10.0)) b1))
  (progn
    (: io fwrite "~p\n" (list f))
    (binary (f float (size 64)))))

πŸ‘‘ ETS AND MNESIA πŸ‘‘

LFE also supports match specifications and Query List Comprehensions. πŸ’–

The syntax for a match specification is the same as for match-lambdas:

(ets-ms
  ((arg ... ) {{(when e ...)}} ...)             - Matches clauses
  ... )

For example:

(ets:select db (ets-ms
                 ([(tuple _ a b)] (when (> a 3)) (tuple 'ok b))))

It's a macro which creates the match specification structure that's used in ets:select and mnesia:select. πŸ”₯

For tracing, instead of the ets-ms macro there's the trace-ms macro, which is also used in conjunction with the dbg module. The same restrictions as to what can be done apply as for vanilla match specifications:

  • There is only a limited number of BIFs which are allowed
  • There are some special functions only for use with dbg
  • For ets/mnesia it takes a single parameter which must be a tuple or a variable
  • For dbg it takes a single parameter which must be a list or a variable
N.B. the current macro neither knows nor cares whether it is being used in ets/mnesia or in dbg. It is up to the user to get this right. πŸ™ˆ (no pressure bestie)

Macros, especially record macros, can freely be used inside match specs. πŸŽ€

CAVEAT Some things which are known not to work in the current version are andalso, orelse and record updates. πŸ’€

⭐ QUERY LIST COMPREHENSIONS ⭐

LFE supports QLCs for mnesia through the qlc macro. It has the same structure as a list comprehension and generates a Query Handle in the same way as with qlc:q([...]). The handle can be used together with all the combination functions in the module qlc. πŸ’œ

For example:

(qlc (lc ((<- (tuple k v) (: ets table e2)) (== k i)) v)
     {{Option}})

Macros, especially record macros, can freely be used inside query list comprehensions. πŸ¦‹

CAVEAT Some things which are known not to work in the current version are nested QLCs and let/case/receive which shadow variables. 😭

✨ Predefined LFE functions ✨

okay so bestie, these are the more-or-less standard lisp functions that come predefined β€” the little starter pack that’s just always there πŸ’–

(<arith_op> expr ...)
(<comp_op> expr ...)

The standard arithmetic operators + - * / and comparison operators > >= < =< == /= =:= =/= can take multiple arguments the same as their standard lisp counterparts. This is still experimental and implemented using macros. They do, however, behave like normal functions and evaluate ALL their arguments before doing the arithmetic/comparisons operations. πŸ‘‘

(acons key value list)
(pairlis keys values {{list}})
(assoc key list)
(assoc-if test list)
(assoc-if-not test list)
(rassoc value list)
(rassoc-if test list)
(rassoc-if-not test list)

The standard association list functions. πŸŽ€

(subst new old tree)
(subst-if new test tree)
(subst-if-not new test tree)
(sublis alist tree)

The standard substitution functions. πŸ¦‹

(macroexpand-1 expr {{environment}})

If Expr is a macro call, does one round of expansion, otherwise returns Expr. ✨

(macroexpand expr {{environment}})

Returns the expansion returned by calling macroexpand-1 repeatedly, starting with Expr, until the result is no longer a macro call. πŸ”₯

(macroexpand-all expr {{environment}})

Returns the expansion from the expression where all macro calls have been expanded with macroexpand. 🌈

NOTE that when no explicit environment is given the macroexpand functions then only the default built-in macros will be expanded. Inside macros and in the shell the variable $ENV is bound to the current macro environment. πŸ‘€
(eval expr {{environment}})

Evaluate the expression expr. Note that only the pre-defined lisp functions, erlang BIFs and exported functions can be called. Also no local variables can be accessed. To access local variables the expr to be evaluated can be wrapped in a let defining these. πŸ’…

For example if the data we wish to evaluate is in the variable expr and it assumes there is a local variable β€œfoo” which it needs to access then we could evaluate it by calling:

(eval `(let ((foo ,foo)) ,expr))

πŸ’– Supplemental Common Lisp Functions πŸ’–

LFE provides the module cl, which contains the following functions that closely mirror functions defined in the Common Lisp Hyperspec. Heads up, bestie: these use zero-based indices, like Common Lisp (unlike Erlang, which start at index 1). A major difference between the LFE versions and the Common Lisp versions of these functions is that the boolean values are the LFE 'true and 'false. Otherwise the definitions closely follow the CL definitions and won’t be documented here. 🫢

cl:make-lfe-bool cl-value
cl:make-cl-bool lfe-bool

cl:mapcar  function  list
cl:maplist  function  list
cl:mapc  function  list
cl:mapl  function  list

cl:symbol-plist  symbol
cl:symbol-name  symbol
cl:get  symbol  pname
cl:get  symbol  pname  default
cl:getl  symbol  pname-list
cl:putprop  symbol  value  pname
cl:remprop  symbol  pname

cl:getf  plist  pname
cl:getf  plist  pname  default
cl:putf  plist  value  pname  ; This does not exist in CL
cl:remf  plist  pname
cl:get-properties  plist  pname-list

cl:elt  index  sequence
cl:length  sequence
cl:reverse  sequence
cl:some  predicate  sequence
cl:every  predicate  sequence
cl:notany  predicate  sequence
cl:notevery  predicate  sequence
cl:reduce  function  sequence
cl:reduce  function  sequence  'initial-value  x
cl:reduce  function  sequence  'from-end  'true
cl:reduce  function  sequence  'initial-value  x  'from-end  'true

cl:remove  item  sequence
cl:remove-if  predicate  sequence
cl:remove-if-not  predicate  sequence
cl:remove-duplicates  sequence

cl:find  item  sequence
cl:find-if  predicate  sequence
cl:find-if-not  predicate  sequence
cl:find-duplicates  sequence
cl:position  item  sequence
cl:position-if  predicate  sequence
cl:position-if-not  predicate  sequence
cl:position-duplicates  sequence
cl:count  item  sequence
cl:count-if  predicate  sequence
cl:count-if-not  predicate  sequence
cl:count-duplicates  sequence

cl:car  list
cl:first  list
cl:cdr  list
cl:rest  list
cl:nth  index  list
cl:nthcdr  index  list
cl:last  list
cl:butlast  list

cl:subst  new  old  tree
cl:subst-if  new  test  tree
cl:subst-if-not  new  test  tree
cl:sublis  alist  tree

cl:member  item  list
cl:member-if  predicate  list
cl:member-if-not  predicate  list
cl:adjoin  item  list
cl:union  list  list
cl:intersection  list  list
cl:set-difference  list  list
cl:set-exclusive-or  list  list
cl:subsetp  list  list

cl:acons  key  data  alist
cl:pairlis  list  list
cl:pairlis  list  list  alist
cl:assoc  key  alist
cl:assoc-if  predicate  alost
cl:assoc-if-not  predicate  alost
cl:rassoc  key  alist
cl:rassoc-if  predicate  alost
cl:rassoc-if-not  predicate  alost

cl:type-of  object
cl:coerce  object  type

Furthermore, there is an include file which developers may want to utilize in their LFE programs: (include-lib "lfe/include/cl.lfe"). Currently this offers Common Lisp predicates, but may include other useful macros and functions in the future. The provided predicate macros wrap the various is_* Erlang functions; since these are expanded at compile time, they are usable in guards. The include has the following: 🌸

(alivep x)
(atomp x)
(binaryp x)
(bitstringp x)
(boolp x) and (booleanp x)
(builtinp x)
(consp x)
(floatp x)
(funcp x) and (functionp x)
(intp x) and (integerp x)
(listp x)
(mapp x)
(numberp x)
(pidp x)
(process-alive-p x)
(recordp x tag)
(recordp x tag size)
(refp x) and (referencep x)
(tuplep x)
(vectorp x)

Non-predicate macros in lfe/include/cl.lfe include:

(dolist ...)
(vector ...)

πŸ¦‹ Supplemental Clojure Functions πŸ¦‹

From LFE’s earliest days, its Lisp-cousin Clojure (created around the same time) has inspired LFE developers to create similar, BEAM-versions of those functions. These were collected in a separate library and then expanded upon, until eventually becoming part of the LFE standard library. So iconic of them, honestly πŸ’œ

Function definition macros

(clj:defn ...)
(clj:defn- ...)
(clj:fn ...)

Threading macros

(clj:-> ...)
(clj:->> ...)
(clj:as-> ...)
(clj:cond-> ...)
(clj:cond->> ...)
(clj:some-> ...)
(clj:some->> ...)
(clj:doto ...)

Conditional macros

(clj:if-let ...)
(clj:iff-let ...)
(clj:condp ...)
(clj:if-not ...)
(clj:iff-not ...)
(clj:when-not ...)
(clj:not= ...)

Predicate macros

(clj:atom? x)
(clj:binary? x)
(clj:bitstring? x)
(clj:bool? x)
(clj:boolean? x)
(clj:even? x)
(clj:false? x)
(clj:falsy? x)
(clj:float? x)
(clj:func? x)
(clj:function? x)
(clj:identical? x)
(clj:int? x)
(clj:integer? x)
(clj:map? x)
(clj:neg? x)
(clj:nil? x)
(clj:number? x)
(clj:odd? x)
(clj:pos? x)
(clj:record? x)
(clj:reference? x)
(clj:true? x)
(clj:tuple? x)
(clj:undef? x)
(clj:undefined? x)
(clj:zero? x)

Other

(clj:str x)
(clj:lazy-seq x)
(clj:conj ...)
(clj:if ...)

Most of the above mentioned macros are available in the clj include file, the use of which allows developers to forego the clj: prefix in calls:

(include-lib "lfe/include/clj.lfe")

πŸ“ Notes

  • NYI β€” Not Yet Implemented
  • N.B. β€” Nota bene (note well)

πŸ‘€ SEE ALSO

lfe(1), lfescript(1), lfe_cl(3)


πŸ‘‘ AUTHORS

Robert Virding.


okay bestie, that’s the whole page β€” source/version vibes: lfe_guide(7), dated 2008-2020. slay and goodbye πŸ’…βœ¨

πŸ“¦ where this page comes from