addAndStringify = \num1, num2 ->
sum = num1 + num2
x = (
if sum == 0 then
""
else if sum < 0 then
"negative"
else
Num.toStr (num1 + num2)
)
Str.concat x "!"
going through the Roc tutorial
if else
as expressions are amazing. Control flow can be simplified a lot this way.
Does the concept of control flow exist in pure functional language?
The concept of control flow implies steps and is imperative. However, because Roc is eager, not lazy, it feels like steps as well, just better.
I think you can even delete the parens around the if expr :smiley:.
If by control flow, you mean branches and loops, yes they exist. You have just demonstrated branches. Loops are harder to find tho. You would need to look past the functional api, into the generated machine code.
Im pretty sure List.walk is implemented as a Tail Recursive function. With tail call optimisation, those specially constrained recursive fn-s compile directly to loops!
I added the () around if to test if it's an expression
if
is only ever an expression in roc
Yes, and you can't have it be without an else branch. That is more wierd to wrap you head around at first. But if you think about it, since you cannot have side effects or mutation of local variables, and there is no "null" or undefined type, it would be incorrect code. The clever use of keywords for if...else
makes this clear, but suppose Roc used braces, just for the example:
x = 2
if 3 == 4 {
x = 3 // x can't be mutated, this if expression would be useless
}
x =
if 3 == 4 {
3
}
/* without an else branch x is not defined to be any value,
but it must be a number, since there isn't a null value in Roc
and the if expression would return a number */
@Norbert Hajagos Is the concept of nothing just a Tag with no payload? Users can define their own concept of nothing but the language does not make decision on it. Am I understanding it correctly?
A 'void returning' function is valid in non pure languages, because they can do side effects. So the function actually does something, it's just not observable from the language's perspective (changing global vars, writing to disk, ... ). There is no such fn is roc. Everything returns something, otherwise it would have no point of existing. Even doing effects (like writing to disk) is about returning a Task that will get executed by the platform.
You rarely use the concept of nothing in Roc. For that, there is the empty record {}
. You use that in like Task.ok {}
for basic-cli as the return value of main. Buuut, you usually do that because you are using the dbg
statement in your program, which is a side effect. Or you are measuring the performance of running a fure function. You wouldn't really use it in production.
A payloadless tag still has a tag, so that would be a byte at least. When talking about nothin values, you usually think of the absence of something. At least when we are talking about expression based languages like Roc. For that, you would use an Option / Maybe type. Or just like how we do that in Roc: just use the Result type.
Look at List.get at roc-lang.org/builtins. The return value is either an Ok value or an Err OutOfBounds. The tag inside the Err tag is there to describe the error, but could have been empty as well. You would know that when you get back an Err, it is the absence of the value you were looking for, aka: a better version of Null.
I think we optimize a single tag variant to nothing as well
SomeTag: [OnlyOneVariantNoData]
x = OnlyOneVariantNoData
x
will be 0 bytes just like an empty record.
Not particularly useful, but just a note. I guess a more useful variant is an empty opaque type cause it can at least opt into abilities.
If it takes 0 bytes, how in the runtime it exists?
Is it compiled away and doesn't exist in the runtime?
Yeah, doesn't exist at runtime
Last updated: Jul 05 2025 at 12:14 UTC