Using FNL

Go to the index

Basic usage

Using FNL is pretty straight-forward. You give it source code, and it gives you HTML.

import fnl

source_code = '''
($
  ((h 1)
    "Hello, world!")
  (p "Lorem ipsum dolor sit amet"))
'''


html: str = fnl.html()

Extending FNL

To extend FNL, you'll need to learn a bit about its internals. The two most important modules are fnl.entities and fnl.entity_types.

fnl.entity_types defines the EntityType class and all of its subclasses; they represent the types that are defined 'in the language spec', if I may say so. You'll need them to define the signatures of your own functions.

fnl.entities defines the Entity class and all of its subclasses; they represent the expressions and values that appear in the program. Here are some of them:

Providing custom symbols

If you take a look at the signature of fnl.html, you can see that it accepts an optional extensions argument. It's a mapping from strings (names) to Entity objects. For example, you can define some constants like that:

import fnl
import fnl.entities as e

source = '''
(p
  "From basic mathematics we know that " (e "pi") " = " PI " and e = " E ".")
'''

print(fnl.html(source, {'PI': e.Integer(3), 'E': e.Integer(3)}))

You should get: <p>From basic mathematics we know that &pi; = 3 and e = 3.</p>

In that mapping, you can provide any expression you want.

print(fnl.html(source, {'PI': e.Integer(3), 'E': e.Name('PI')}))

Creating functions

Let's create a very simple function: it should take a string as an argument and return it unchanged. First, let's import all the necessary modules.

>>> import fnl
>>> import fnl.entities as e
>>> import fnl.entity_types as et

Then we'll need to create a signature for our function so that the type system knows what the function accepts and returns. As you may remember, a function can take a few required arguments and (possibly) varargs of one type, like (λ a b ...c . d).

>>> function_type = et.TFunction(
...    arg_types = ( et.TStr(), )
...    rest = None,
...    return_type =  et.TStr()
... )
...
>>> function_type.signature()
'(λ str . str)'

Now we have to create a callable (basically, a Python function) that takes a e.String() and returns a e.String().

>>> def identity(s: e.String) -> e.String:
...     return s
...
>>>

The type annotations are completely optional, they only serve as documentation.

Now we can combine the two to create a FNL function:

>>> function = e.Function({function_type: identity})
>>>

e.Function is a subclass of e.Entity —it's the type that all the built-in functions in FNL have. Let's test our function.

>>> fnl.html('(id "hello")', {'id': function})
'hello'
>>> fnl.html('(p (id "hello, ") (id "world!"))', {'id': function})
'<p>hello, world!</p>'

Congratulations, it works! Now let's try to break our function.

>>> fnl.html('(id 42)', {'id': function})
FnlTypeError: Cannot call (λ str . str) with (int) (line 1, column 1)

Using string-based function annotations

Creating a function type is so verbose, why can't I just write (λ str . str)? Actually, you can! fnl.type_parser.parse_fn converts a function type as a string into a TFunction.

from fnl.type_parser import parse_fn

def identity(s):
    return s

function = e.Function({parse_fn('(λ str . str)'): identity})

Examples and more advanced features

Let's create a dup function that repeats a string twice using the same steps.

def _dup(s: e.String) -> e.String:
    return e.String(s.value + s.value)

fnl.html('(dup "py")', {parse_fn('(λ str . str)'): dup})
#=> 'pypy'

Let's create a function - that accepts any number of strings and puts hyphens between them.

def _hyphenate(*strings: e.String) -> e.String:
    return e.String("-".join(s.value for s in strings))
hyphenate = e.Function({parse_fn('(λ ...str . str)'): _hyphenate})

fnl.html('(- "lorem" "ipsum" "dolor" "sit" "amet")', {'-': hyphenate})
#=> 'lorem-ipsum-dolor-sit-amet'

fnl.html('(type -)', {'-': hyphenate})
#=> '(λ  ...str . str)'

Remember that a function can have multiple overloads. Let's modify our function so that it accepts integers as well.

def _hyphenate_str(*strings: e.String) -> e.String:
    return e.String("-".join(s.value for s in strings))

def _hyphenate_int(*ints: e.Integer) -> e.String:
    return e.String("-".join(str(n.value) for n in ints))

hyphenate = e.Function({
    parse_fn('(λ ...str . str)'): _hyphenate_str,
    parse_fn('(λ ...int . str)'): _hyphenate_int,
})

fnl.html('(- "lorem" "ipsum" "dolor" "sit" "amet")', {'-': hyphenate})
#=> 'lorem-ipsum-dolor-sit-amet'

fnl.html('(- 1 2 3 4 5)', {'-': hyphenate})
#=> '1-2-3-4-5'

fnl.html('(type -)', {'-': hyphenate})
#=> '(λ  ...str . str)|(λ  ...int . str)'

Using the fnl.definitions.fn shortcut

If you look at the source code of fnl/definitions.py, you'll see declarations like this:

@fn(BUILTINS, "$")
def concat():
    def from_inline(*args):
        return e.InlineConcat(args)
    yield ("(λ  ...inline . inline)", from_inline)

    def from_mixed(*args):
        return e.BlockConcat(args)
    yield ("(λ  ...inline|block . inline|block)", from_mixed)

fnis a helper decorator for creating FNL function. It accepts a dictionary and the function name as argument, and it can be applied to a generator function that yields (signature, function) tuples:

For example, our - function could've been written as this:

extensions = {}

@fn(extensions, "-")
def hyphenate():
    def from_strings(*strings):
        return e.String("-".join(s.value for s in strings))
    yield ("(λ ...str . str)", from_strings)

    def from_ints(*ints):
        return e.String("-".join(str(n.value) for n in ints))
    yield ("(λ ...int . str)", from_ints)

fnl.html('(- 1 2 3 4 5)', extensions  )
#=> '1-2-3-4-5'


Source:

($docs $filename $source "Using FNL"
  ((h 2)
    "Basic usage")
  (p
    "Using FNL is pretty straight-forward. You give it source code, and it
    gives you HTML.")

  (pre """
  import fnl

  source_code = '''"""($fnl
"""    ($
      ((h 1)
        "Hello, world!")
      (p "Lorem ipsum dolor sit amet"))
  """)"""  '''


  html: str = fnl.html()
  """)


  ((h 2)
    "Extending FNL")

  (p
    "To extend FNL, you'll need to learn a bit about its internals.
    The two most important modules are " (tt "fnl.entities") " and "
    (tt "fnl.entity_types") ".")

  (p
    (tt "fnl.entity_types") " defines the " (tt "EntityType") " class and all
    of its subclasses; they represent the types that are defined 'in the
    language spec', if I may say so. You'll need them to define the signatures
    of your own functions.")

  (p
    (tt "fnl.entities") " defines the " (tt "Entity") " class and all
    of its subclasses; they represent the expressions and values that appear
    in the program. Here are some of them:")

  (list-unordered
    ($ (tt "Name ") (--) " represents an expression that accesses a global
      value, like " (tt "bf") " or " (tt "$") ".")

    ($ (tt "Integer ") (--) " represents... well, an integer")

    ($ (tt "String ") (--) " as you might expect, it's a string")

    ($ (tt "InlineTag ") (--) " inline HTML tag, such as <b>")

    ($ (tt "BlockTag ") (--) " block HTML tag, such as <p>")

    ($ (tt "InlineConcat ") (--) " concatenation of inline elements")

    ($ (tt "BlockConcat ") (--) " concatenation of mixed elements; the
      result is a block element. The name might be confusing, but
      the relevant part here is that this value cannot be used as an inline
      element, but can be used as a block element.")
  )


  ((h 2)
    "Providing custom symbols")
  (p
    "If you take a look at the signature of " (tt "fnl.html") ", you can see
    that it accepts an optional " (tt "extensions") " argument. It's a mapping
    from strings (names) to " (tt "Entity") " objects. For example, you can
    define some constants like that:")

  (pre """
  import fnl
  import fnl.entities as e

  source = '''"""($fnl
"""  (p
    "From basic mathematics we know that " (e "pi") " = " PI " and e = " E ".")
""")"""  '''

  print(fnl.html(source, {'PI': e.Integer(3), 'E': e.Integer(3)}))
  """)

  (p
    "You should get: " (mono "<p>From basic mathematics we know that &pi; = 3 and e = 3.</p>"))

  (p
    "In that mapping, you can provide any expression you want.")

  (pre """
  print(fnl.html(source, {'PI': e.Integer(3), 'E': e.Name('PI')}))
  """)


  ((h 2)
    "Creating functions")

  (p
    "Let's create a very simple function: it should take a string as an argument
    and return it unchanged. First, let's import all the necessary modules.")

  (pre """
    >>> import fnl
    >>> import fnl.entities as e
    >>> import fnl.entity_types as et
  """)

  (p
    "Then we'll need to create a " (bf "signature") " for our function so that
    the type system knows what the function accepts and returns. As you may
    remember, a function can take a few required arguments and (possibly)
    varargs of one type, like " (mono "(λ a b ...c . d)") ".")

  (pre """
    >>> function_type = et.TFunction(
    ...    arg_types = ( et.TStr(), )
    ...    rest = None,
    ...    return_type =  et.TStr()
    ... )
    ...
    >>> function_type.signature()
    '(λ str . str)'
  """)

  (p
    "Now we have to create a callable (basically, a Python function) that takes
    a " (mono "e.String()") " and returns a " (mono "e.String()") ".")

  (pre """
    >>> def identity(s: e.String) -> e.String:
    ...     return s
    ...
    >>>
  """)

  (p
    "The type annotations are completely optional, they only serve as documentation.")

  (p
    "Now we can combine the two to create a FNL function:")

  (pre """
    >>> function = e.Function({function_type: identity})
    >>>
  """)

  (p
    (mono "e.Function") " is a subclass of " (mono "e.Entity ") (--)
    "it's the type that all the built-in functions in FNL have. Let's test our function.")

  (pre """
    >>> fnl.html('(id "hello")', {'id': function})
    'hello'
    >>> fnl.html('(p (id "hello, ") (id "world!"))', {'id': function})
    '<p>hello, world!</p>'
  """)

  (p
    "Congratulations, it works! Now let's try to break our function.")
  (pre """
    >>> fnl.html('(id 42)', {'id': function})
    FnlTypeError: Cannot call (λ str . str) with (int) (line 1, column 1)
  """)


  ((h 2)
    "Using string-based function annotations")

  (p
    "Creating a function type is so verbose, why can't I just write (λ str . str)?
    Actually, you can! " (mono "fnl.type_parser.parse_fn") " converts a function
    type as a string into a " (tt "TFunction") ".")

  (pre """
    from fnl.type_parser import parse_fn

    def identity(s):
        return s

    function = e.Function({parse_fn('(λ str . str)'): identity})
  """)


  ((h 2)
    "Examples and more advanced features")

  (p
    "Let's create a " (tt "dup") " function that repeats a string twice using
    the same steps.")

  (pre """
    def _dup(s: e.String) -> e.String:
        return e.String(s.value + s.value)

    fnl.html('(dup "py")', {parse_fn('(λ str . str)'): dup})
    #=> 'pypy'
  """)

  (p
    "Let's create a function " (tt "-") " that accepts any number of strings
    and puts hyphens between them.")

  (pre """
    def _hyphenate(*strings: e.String) -> e.String:
        return e.String("-".join(s.value for s in strings))
    hyphenate = e.Function({parse_fn('(λ ...str . str)'): _hyphenate})

    fnl.html('(- "lorem" "ipsum" "dolor" "sit" "amet")', {'-': hyphenate})
    #=> 'lorem-ipsum-dolor-sit-amet'

    fnl.html('(type -)', {'-': hyphenate})
    #=> '(λ  ...str . str)'
  """)

  (p
    "Remember that a function can have multiple overloads. Let's modify our
    function so that it accepts integers as well.")

  (pre """
    def _hyphenate_str(*strings: e.String) -> e.String:
        return e.String("-".join(s.value for s in strings))

    def _hyphenate_int(*ints: e.Integer) -> e.String:
        return e.String("-".join(str(n.value) for n in ints))

    hyphenate = e.Function({
        parse_fn('(λ ...str . str)'): _hyphenate_str,
        parse_fn('(λ ...int . str)'): _hyphenate_int,
    })

    fnl.html('(- "lorem" "ipsum" "dolor" "sit" "amet")', {'-': hyphenate})
    #=> 'lorem-ipsum-dolor-sit-amet'

    fnl.html('(- 1 2 3 4 5)', {'-': hyphenate})
    #=> '1-2-3-4-5'

    fnl.html('(type -)', {'-': hyphenate})
    #=> '(λ  ...str . str)|(λ  ...int . str)'
  """)


  ((h 2)
    "Using the " (mono "fnl.definitions.fn") " shortcut")
  (p
    "If you look at the source code of " (mono "fnl/definitions.py")
    ", you'll see declarations like this:")

  (pre """
    @fn(BUILTINS, "$")
    def concat():
        def from_inline(*args):
            return e.InlineConcat(args)
        yield ("(λ  ...inline . inline)", from_inline)

        def from_mixed(*args):
            return e.BlockConcat(args)
        yield ("(λ  ...inline|block . inline|block)", from_mixed)
  """)

  (p
    (mono "fn") "is a helper decorator for creating FNL function. It accepts
    a dictionary and the function name as argument, and it can be applied to a
    generator function that yields " (mono "(signature, function)") " tuples:")

  (p
    "For example, our " (tt "-") " function could've been written as this:")

  (pre """
    extensions = {}

    @fn(extensions, "-")
    def hyphenate():
        def from_strings(*strings):
            return e.String("-".join(s.value for s in strings))
        yield ("(λ ...str . str)", from_strings)

        def from_ints(*ints):
            return e.String("-".join(str(n.value) for n in ints))
        yield ("(λ ...int . str)", from_ints)

    fnl.html('(- 1 2 3 4 5)', extensions  )
    #=> '1-2-3-4-5'
  """)

  (horizontal-rule)
  ((h 2) "Source:")
  (pre ($fnl $source))
)