DocsLanguage
Functions.
Define calculations, pass functions as values, and express repetition with recursion.
A function gives a name to a calculation. Its arguments and result are ordinary values, and functions are values too: you can pass one to another function, store it, or return it.
Define and call a function.
add : Int -> Int -> Int
add left right = left + right
main() = print (add 20 22)
This prints 42. Calls are written with spaces, not with comma-separated arguments in parentheses. The annotation lists two arguments and then the result. Arrows group to the right, so Int -> Int -> Int means Int -> (Int -> Int).
That grouping has a nice consequence: functions are curried. Give add only one argument and you get back a function that’s waiting for the other:
add : Int -> Int -> Int
add left right = left + right
main() =
addTen = add 10
print (addTen 5)
This prints 15. add didn’t need any special declaration to allow this.
Application and lambdas.
For a short callback, write a lambda:
import List
main() = print (List.map { value -> value * 2 } [1, 2, 3])
A lambda can take several arguments, as in { left right -> left + right }, and its closing brace ends the body. The shorthand { expression } means { () -> expression }, and the expression is evaluated when that Unit function is called.
A function that takes Unit is normally called as action(). The empty parentheses attach tightly, so print action() means print (action ()), while with a space, print action () means (print action) (). Stick to the attached spelling for a Unit call.
Read a pipeline.
import List
main() =
answer = [1, 2, 3] |> List.map { value -> value * 2 } |> List.foldl (+) 0
print answer
This prints 12. A pipeline reads left to right: value |> function passes the value to the function, so each step feeds the next. function <| value goes the other way and passes the expression on the right to the function on the left. Both are plain, strict calls, and any effects happen when the function you call performs them. If an expression mixes the two directions, add parentheses.
Function equations.
A function can have one equation for each shape of input:
withDefault fallback Nothing = fallback
withDefault _ (Just value) = value
main() = print (withDefault 0 (Just 42))
This prints 42. _ means “I don’t need this value”, and parentheses group a constructor pattern such as (Just value). The equations have to sit next to each other, take the same number of arguments, and together cover every possible input with no redundant rows. Blank lines and comments between rows are fine, and any other declaration ends the group.
Types and patterns goes into pattern matching in more depth.
Inference and annotations.
Fango infers types, so you don’t have to write them. An annotation is still worth adding when you want to state a contract, and it helps readers of your public functions. A lowercase type variable stands for any type, and a constraint says what operations that type must support:
double : Num a => a -> a
double value = value + value
main() =
print (double 21)
print (double 1.5)
This prints 42 and 3. Num a allows numeric operations on a, and the variable’s name has no special meaning. Type classes explains constraints and instances.
Visibility and recursion.
Top-level named functions can call functions declared later in the file, including each other. Ordinary values and local bindings are available only after their declarations. A parameter or local name can’t shadow a module function’s name.
Fango has no loops, so repetition is written with recursion:
sumTo : Int -> Int
sumTo limit = loop limit 0
loop : Int -> Int -> Int
loop remaining total =
if remaining <= 0 then total else loop (remaining - 1) (total + remaining)
main() = print (sumTo 100)
This prints 5050.
Tail-call guarantee.
You might worry that a long recursion will run out of stack. A direct, fully applied self-call in tail position runs in constant stack, as long as it meets the language’s tail-call rules. The call to loop above is the whole result of its branch, with nothing left to calculate after it returns, so it qualifies.
1 + loop next total is different, because the addition still has to happen after the call returns. Mutual recursion, calls inside a lambda, and calls under a handler aren’t covered by the guarantee, and capturing changing loop parameters in callbacks can also prevent the optimization. When a fold or traversal already expresses the loop you need, use that instead.
Row-kinded parameters.
An effect row can be passed as a type argument, as long as the declaration uses that parameter as an effect-row tail. For example, Reader {Console} describes a reader whose operations read standard input through the console. This is a different thing from an ordinary value-type parameter such as the a in List a.
Scoped callbacks.
Some runners give their callback a fresh local permission. A memory reader is a good example:
import Bytes
import Reader
firstTwo text =
Reader.withBytes (Bytes.fromString text) { reader ->
Bytes.toString (Reader.readUpTo reader 2)
}
main() = print (firstTwo "Fango")
Just "Fa"
firstTwo returns plain parsed data, so it stays usable after the reader’s callback ends. What you can’t do is return reader itself, one of its operation functions, or a stream that advances it, because that would carry the reader’s private permission out of the callback, and Fango rejects it. Wrapping the reader in another container doesn’t hide the permission.
The runner removes its local permission and passes your other effects through. Resources and scopes explains how this static rule differs from closing a file or socket.
To write a runner of your own that passes a scoped callback along, end the callback’s row with a scoped row variable, written #s:
import Bytes
import Reader exposing (Reader)
withText : String -> (Reader {#s} ->{#s} a) ->{e} a
withText text action = Reader.withBytes (Bytes.fromString text) action
main() =
print (withText "Fango" { reader -> Bytes.toString (Reader.readUpTo reader 2) })
Just "Fa"
At each call, #s stands for the callback’s fresh permission plus everything on the runner’s own final arrow, here whatever e is for the caller. That’s what lets the caller’s other effects through, so nothing else is written beside #s. Every occurrence keeps the #, including the one inside Reader {#s}, and #s is always the last entry of its row. Effects written before it, as in ->{Fail String, #s} a, are granted to the callback, and the runner has to handle them. Scoped variables work on annotated top-level runners, not on local annotations, record fields, effect operations, or class methods. Without the #, {s} is an ordinary row variable.
The scoped callback doesn’t have to be a runner’s only callback, or its first. use always fills the last parameter. In use Client.mock serverHandler, the handler is a scoped callback passed explicitly, and use supplies the client action after it. The HTTP client guide shows this in practice.
