Just regular numbers, like
5
7.3
4.524e13
-10.6E-20
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.
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:
- directly
1 + 2 foo ? (yes, no)
- as functions
(+) 1 2
Names can be called in two ways, too:
- as operators
1 `add` 2 3 `between` (4, 9) list `len` ()
- 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...
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))
}
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
}
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.
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)])