Using ambient variables, program behavior traditionally considered "side effect" can be expressed easily. It allows you to express, ambient authority, global variables, context variables with the same idea.
the whole point of pure functional programming is to not do that exact thing, right?
Well, it has monad, and I think it's counter-intuitive.
monads aren't really tied to functional programming (at least in theory, in practice I guess they are)
Here's a simple example with strange syntax for doing operations on array.
a = [1, 2, 3, 4, 5, 98, 99]
sumbelow10 : (List[int]) -> (int)
sumbelow10 (ambient List[int]) = (ambient int)
filter it -> it <= 10
sum
result = sumbelow10 a
# result would be 15
what is this meant to show?
If you write it like this
sumbelow10 (ambient List[int]) = (ambient int)
filter it -> it <= 10
0
the compiler will complain with
inside sumbelow10:
List[int] returned by filter is made ambient isn't used afterwards
Basically,
f a b is the same as f b aIn the above example, filter has the signature List[int] -> (int -> bool) -> List[int], and the compiler just fills in the first argument.
Realistic example of using ambient variables to reduce boilerplate:
Raylib is a c library for making games with a single window.
Here is Raylib API for reference.
"explicit is better than implicit"
Its API is designed for ease of use, but isn't quite correct, because it allows you to do this:
Texture2D texture = LoadTexture("example.png");
InitWindow(800, 600, "Hello world");
The program type checks but SIGSEGV without any error message. The problem? LoadTexture requires GPU access, which is provided by an invisible global variable, and you have to call InitWindow to initialize GPU for rendering.
You can make the variable representing GPU access not global by making the library user have to write this:
Context ctx = InitWindow(800, 600, "Hello world");
Texture2D texture = LoadTexture(ctx, "example.png");
But you need to write ctx every time you need to draw a line on screen, draw an image, or read a file. You know you have the permission to draw something, but the compiler is too dumb to remember anything between function calls unless you assign it to a variable.
This is even more problematic when you don't have mutable variables. You have to write this:
Texture2D texture, Context ctx = LoadTexture(ctx, "example.png");
Haskell's solution to this is Monad. It is very hard to understand and use; for example, you have to define a new type of monad for array operation to be able to write sumbelow10 like above.
It can also represent "side effects".
in haskell you can also totally write something like this, without knowing anything about monads
a = [1, 2, 3, 4, 5, 98, 99]
sumBelow10 = \list ->
list
|> List.keepIf (\element -> element < 10)
|> List.sum
(using roc syntax here btw)
To handle "side effects", we must also be able to mark a type as unique (cannot be copied).
This is used in the programming language Clean.
https://en.wikipedia.org/wiki/Uniqueness_type
actually in haskell you can even do sumBelow10 = sum . filter (< 10)
I know, I did a whole thesis on it :smiley:
Folkert de Vries said:
in haskell you can also totally write something like this, without knowing anything about monads
a = [1, 2, 3, 4, 5, 98, 99] sumBelow10 = \list -> list |> List.keepIf (\element -> element < 10) |> List.sum
How do you interleave |> on different variables?
its a function, so sumBelow10 a then later sumBelow10 [ 1,3,3,4,5,6,5,6,7,8,88, ... ]
I mean something like using different authorities to do different things.
Let's say you want to read a file and draw it's content as text on the screen, take a screenshot of that and save as an image.
You would need a variable for File IO and one for rendering (which depends on a buffer).
My idea is some type of compiler trick that turns
ctx = newRenderingContext
ctx = drawLine ctx ((0, 0) (0, 1))
ctx = drawLine ctx ((0, 0) (1, 0))
into
newRenderingContext
drawLine ((0, 0) (0, 1))
drawLine ((0, 0) (1, 0))
sure, well the simple answer is
newRenderingContext
|> drawLine ((0, 0) (0, 1))
|> drawLine ((0, 0) (1, 0))
but that only works if drawLine really returns a new ctx and not some Task Ctx * or IO Ctx
think writing simple functions and letting compiler keep track of "global" state is easier to write and learn what is monad.
Folkert de Vries said:
sure, well the simple answer is
newRenderingContext |> drawLine ((0, 0) (0, 1)) |> drawLine ((0, 0) (1, 0))but that only works if
drawLinereally returns a newctxand not someTask Ctx *orIO Ctx
In Roc's case, drawLine is provided by the platform.
I think you'll really like effect handlers
those are not in roc, but are in e.g. unison or koka
and they do this while guaranteeing that ultimately the "global"/"hidden" state is always there (no segfaults or other bad things)
thanks
Last updated: Jun 16 2026 at 16:19 UTC