Skip to content

Instantly share code, notes, and snippets.

@Heimdell
Last active September 22, 2018 16:50
Show Gist options
  • Save Heimdell/f339c3eada393080c842afffc250da7d to your computer and use it in GitHub Desktop.
Save Heimdell/f339c3eada393080c842afffc250da7d to your computer and use it in GitHub Desktop.

Syntax

Elements

Constants

Numbers

Just regular numbers, like

    5
    7.3
    4.524e13
    -10.6E-20

String

This is a string:

    "hello, world"

And that's how we escape " in strings:

    "hello, ""so called"" world"

Also, if you write some strings literals sequentally, they will be concatenated:

    "hello, " "world" == "hello, world"

Strings are multiline.

From technical standpoint, " is inserted between two "-delimited blocks, if there is no space between them.

Names and operators

These are names:

    a
    lets-go
    length
    foo13123
    is-good?

These are operators:

    +
    -
    >>=
    +?
    +one

The first letter determines what category the name is from.

Operators can be called in two ways:

  1. directly
        1 + 2
        
        foo ? (yes, no)
  2. as functions
        (+) 1 2

Names can be called in two ways, too:

  1. as operators
        1 `add` 2
        
        3 `between` (4, 9)
        
        list `len` ()
  2. as normal functions
        add(1, 2)

Both names and operators can be qualified. It looks like this:

    math.log(n, base)
    n `math.log` base
    
    1 fast-math.+ 2
    (fast-math.+) 1 2

Also, if lets say, >>= operator is declared as a method, another option is possible:

    box .>>= callback

Warning! Operators are all right-associative and have no priorities! So,

    2 + 3 * 5 == 25

Use parens,

    2 + (3 * 5)

or...

Constructions

Let/where-expressions

Sometimes, you may want to make local definitions. That's how you do it:

    let
        ; a = 1
        ; b = 2
        ; f(c) = c + b
    in
        f

Thus the result of the expression will be function f, and names a and b will be hidden from the rest of the program.

There is an alternative syntax, so two following expressions are declaring the same thing:

    let
        ; foo(c, 6) -> bar
    in
        bar

    let
        ; bar = foo(c, 6)
    in
        bar

There is also where-expression:

    ; equation-root (a, b, c) =
        pair.from(
            , - D + 2 * a / 2 * c
            , + D + 2 * a / 2 * c
        )
        where {
            ; D = math.sqrt(b * b - (4 * a * c))
        }

Objects

That's how you declare an object:

    { a = 1
    ; b = 2
    ; f (c) =
        let d = 5
        in  b + c - d
    }

You can use both ; and , as separators.

You can place additional separator before first field, like that:

    {
    , a = 1
    , b = 2
    }

That's good for moving first field anywhere in the object.

Objects can also have a tag, which can be thought as a "type" field.

    Pair { fst = 1, snd = "2" }

"Object" part can be omitted:

    True

Yes, this is an object. Yes, booleans are essentially objects.

The tag allows you to pattern-match on object, exposing fields you want:

    ; sum-pair(pair) =
        case pair of {
        |        Both { fst = it, snd } -> it + snd
        | res as Left { fst }           -> res.fst + fst
        |        Nothing                -> def
        }

If object does have the tag you asked but no field you wanted (its own field, not one from prototype), its an error.

You cannot match anything from the prototype.

It is also possible to match inside let-statements, but if the matching fails, its an error.

    let
        map.lookup(key) -> Just { fromJust = val }
    in
        val

You can also attach prototype to object, which should be an object, too.

    ; boolean = {
        ; true  = True  as boolean
        ; false = False as boolean
        
        ; bool.not() =
            case bool of {
            | True  -> false
            | False -> true
            }
    }

This will allow you to call .not() on any boolean, coming from boolean module.

(Yes, values can refer to themselves. No, it will not cause infinite loop)

You can have objects inside objects:

    parser = {
        ; input-stream = {
            ; from-text (text) = ...
            ; ...
        }
        
        ; position = {
            ; start = ...
            ; ...
        }
        
        ; run-parser (text) = ...
    }

If object sees a name in its scope, it can declare it as captured: that means, object will have field with the same name and the same value:

{
    ; a = 1

    ; b = {
        ; a
        ; c = b.a + 1
    }

}

Here, b.a is captured.

This allows for constructor-like functions, like that:

; pair = {
    ; make (fst, snd) = Pair { fst, snd } as pair
    
    ; pair.swap() = make (pair.snd, pair.fst)
}

Objects here carry a role of modules, separating details from each other.

Sometimes, you want to extract subobject into another file. In that case, you should be able to check if anything breaks before moving it. To do that, you add a module keyword before object definition:

    parser = {
        ; module input-stream = {
            ; from-text (text) = ...
        }

        ; ...        
    }

In that case, input-stream can only refer to itself and to things inside it.

Since no code can live in such a vacuum, we introduce another keyword, import:

    parser = {
        ; module input-stream = 
            import
                ; boolean -> {
                    ; true
                    ; false
                }
            
                ; list
            in {
                ; from-text (text) = ...
            }
            
        ; ...        
    }

It allows you to refer to other things outside of the module scope.

Toplevel imports will try to interpret object names as relative filepaths; the special name world allows to refer to global mount point for all visible modules:

    import
        ; world -> {
            ; data -> {
                ; list
                ; map
                ; boolean -> {
                    ; true
                    ; false
                }
                ; string
            }
            
            ; control -> {
                ; promise
            }
            
            ; text.search -> {
                ; regex
            }
        }
        
        ; parser -> {
            run-parser
        }
    in {
        ; foo = true.not()
    }

Here, if data.dawg is a single file at the project root, the evaluator will attempt to retrieve fields list, map, etc from it. If data is a directory, it will go inside in search for list, map, etc instead.

If you need "private" fields, you can emulate them as let- or where-expressions, like that:

    ; position = {
        ; start = make (1, 1, 0)
        
        ; pos.next(char) = ...
    }
      where {
        ; make (line, col, offset) = Pos {
            line, col, offset
        } as position
      }

Lists

This is a couple of list literals:

    []
    [a]
    ["hel", "lo, ", "wor", "ld!"]
    [1, 2, ... 10]
    [1, 2, ...]

Yes, the last one is an infinite list. No, it will not cause any problem, unless you call .length() on it.

These literals will be transformed by preprocessor into, accordingly:

    list.nil
    a `list.cons` list.nil
    "hel" `list.cons` ("lo, " `list.cons` ("wor" `list.cons` ("ld!" list.nil)))
    list.generate-from-step-to(1, 2, 10)
    list.generate-from-step(1, 2)

So, if you import something fancy with the name of 'list', you can have fancy lists.

Maps

Yes, we have maps. Becauce calling absent field of an object is an error. And you can't check if object has the field in question.

These are maps:

    :{ 1: 2, 3: 4 }
    
    :{ foo: bar, qux: foo }
    
    :{ "a": 1, "b": 2 }

Notice that in second example the keys are values of names foo and qux, not the names itself!

Preprocessor will turn it into:

    map.from-list([map.kv-pair(1, 2), map.kv-pair(3, 4)])

    map.from-list([map.kv-pair(foo, bar), map.kv-pair(qux, foo)])

    map.from-list([map.kv-pair("a", 1), map.kv-pair("b", 2)])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment