DocsGetting started

The REPL.

Try expressions and declarations in a persistent interactive session.

The REPL is the place for quick experiments. Start it with:

fango repl

Type an expression and press Enter. The REPL prints its value, then its type:

> 6 * 7
42 : Num a => a
> 1 + 1.5
2.5 : Float
> "hello"
"hello" : String

42 has the type Num a => a, which reads as “any type a that is a number”. Fango hasn’t had to pick one yet, so it stays general. Mixing in 1.5 settles it as Float.

A definition prints its name and inferred type, and you can use it straight away:

> square x = x * x
square : Num a => a -> a
> square 7
49 : Num a => a

You didn’t write a type annotation, and Fango worked one out anyway. That makes the REPL a handy way to see what it makes of your code.

Inspect a type.

To see a type without running anything, put :type before an expression. It prints only the type:

> :type square
Num a => a -> a
> :type [1, 2, 3]
Num a => List a
> :type "hi"
String

The prompt starts with the same everyday names a source module gets. To set the local module root yourself, pass a directory: fango repl path/to/project.

Imports.

To use a standard-library module’s qualified names, import it first:

> import List
> List.map { value -> value * 2 } [1, 2, 3]
[2, 4, 6] : Num a => List a
> :type List.map
(a ->{e} b) -> List a ->{e} List b

Imports, aliases, and exposing lists work just as they do in files. Local dotted module names map to paths under the session’s source root. Imports accumulate as you go, and if one fails, none of its partial changes are kept.

Declarations and redefinition.

You can define values, functions, types, effects, classes, and instances right at the prompt. Indented input can continue over several lines, and a blank continuation line submits it.

Names you define can be redefined later. One thing to watch for: values and closures you already created may keep the earlier binding, so redefining a helper doesn’t necessarily change older values that used it. Redefining a type creates a new, distinct type. Names that come from an import or the prelude can’t be redefined this way, and neither can classes.

If you want a function written as several adjacent pattern equations, use a source file. The prompt doesn’t gather those into one grouped definition.

Evaluation and commands.

Expressions with effects run as soon as you enter them. A value definition whose body performs effects also runs once, when you enter it, and keeps the result:

> x = print 1
1
x : ()
> x
() : ()

Using x later doesn’t print again, but redefining it does. If the body fails, the name stays as it was. A function that performs effects waits until you call it.

Command Action
:type expression Inspect a type without evaluating
:reload Refresh imported local modules
:help Show command help
:uses List installed handler levels
:end End the innermost handler level
:quit or :q Leave the session

The prompt handles each input’s Fail effects for you: it reports the failure and carries on with the session. Services get their default handlers, as they do for a program’s main: print writes to the session’s terminal and Random.int draws from system entropy. Other effects need their own handlers, or a handler level, which takes precedence over a service’s default. If an input fails with an error, everything you entered before it is still available.

Reloading modules.

When you edit an imported local module, :reload brings its new code into the session. For example, save this as Maths.fango in the session’s source root:

module Maths exposing (adjust)

adjust : Int -> Int
adjust number = number + 1

Import it and define a helper at the prompt:

> import Maths
loaded Maths
> f x = Maths.adjust x
f : Int -> Int
> f 10
11 : Int

Now change number + 1 to number + 2 in the file and reload:

> :reload
reloaded Maths
retained earlier module bindings: f
> Maths.adjust 10
12 : Int
> f 10
11 : Int

New expressions see the updated module. Definitions you already made at the prompt keep the code they were created with, so f still adds one. Redefine f to pick up the new version. Values you’ve already computed stay as they are, and effectful value definitions don’t run again. Reload lists the affected definitions so you can see which ones are stale.

Reload checks the changed modules and their dependencies before installing anything. If that fails, it prints reload failed; session unchanged and your session carries on as before, so you can fix the file and try again. If nothing changed, it prints nothing to reload.

Your imports, aliases, and exposed names survive a reload. If a module no longer exports a name you listed explicitly, the name is dropped and reported. A wildcard import simply follows the new interface.

End any handler levels with :end first, or reload stops with RELOAD WITH ACTIVE HANDLERS. Bundled library sources can’t be reloaded, and neither can changes to native sidecars: those report RELOAD REQUIRES RESTART and need a fresh session.

If you change a type, effect, or class that earlier values were built with, those values can stop fitting the new functions. Reload keeps the old versions available, marking retained type and effect names with before reload, so existing values keep working until you restart.

Handler levels.

Sometimes you want to try several operations against the same handler. A use item installs a callback-based handler for the inputs that follow. For example, once State is imported, use State.run 0 keeps that state runner active until you enter :end. The prompt shows how many levels are active.

Notice the prompt changing to 1> while the handler is active. Try these inputs in order:

> import State
loaded State
> use State.run 0
1> State.put 7
() : ()
1> State.get()
7 : Int
1> :end
StateResult { value = (), state = 7 } : StateResult Int ()
>

The get sees the value that put stored.

The StateResult appears because :end finishes the State.run. A State.run normally hands back a StateResult when it completes, holding the value the run produced and the final state. Here the run lasted for the whole level, so you see it when the level closes: state = 7 is what put left behind, and value is () because the level itself produces nothing. Pressing Ctrl-D at the prompt ends the innermost level too, so it works in place of :end.

After that the handler is gone, and any later state request needs a new runner. Without use, you’d have to call State.run with a block containing everything you want to try, and enter it all at once. Each separate State.run starts from scratch, so state can’t carry over from one input to the next.

A definition that only performs what a level handles, such as n = State.get() under use State.run 0, runs once when you enter it and keeps its value after the level ends. A scoped runner can also bind a reader for the length of a level. Names that depend on that local permission disappear when the level ends. Ending a level runs the runner’s cleanup, and quitting ends the levels from the inside out.

This helps with repeated requests under Http.Client.run, or with experiments on state. You won’t need it for ordinary arithmetic or pure function definitions.

Line editing.

In a terminal you get cursor movement, history, reverse search, and Tab completion. Tab completes a command after :, a module path after import, and otherwise a keyword or a name in scope. Qualified names complete one segment at a time, so Li offers List. and then List.ma offers List.map. The first Tab inserts the longest common completion, and a second lists every candidate. History is saved in ~/.fango_history; lines your program reads aren’t added to it.

Ctrl-C clears unfinished input, or interrupts the evaluation that’s running. Cancelling async work is cooperative, so CPU-bound loops need checkpoints, and arbitrary blocking native work can delay the return. Ctrl-D ends the innermost handler level, or exits if there isn’t one.

Debugging.

fango repl --debug starts a session where you can set breakpoints with :break FILE:LINE and step through an expression with :step EXPR. Debugging covers the commands.