Functional Friday - Episode 3
Learning Rescript - Introducing functions
Welcome back to Functional Friday, the place where a function didn't
return because someone forgot to call her back 🥁! In the
last episode
we learnt how important expressions are
in ReScript and how to use them at a very basic level, including how
to define and use
if
expressions.
Keeping up with this topic, in this week episode we'll see another
form of expressions: functions. If you're here for the first time, I
highly suggest you to read at least the second episode so you don't
get lost too much (hopefully 😅).
As a disclaimer, this episode will be quite technical hence more dense
than previous ones; this means that you may find it harder to digest
it as a whole: if this is the case, please take your time to get
through it little by little.
We're here for learning new things, without any rush.
3.1 Function declaration
As said while talking about expressions, declaring a new function in ReScript looks really familiar for anyone having experience in JavaScript/TypeScript. In fact, you can declare a new function by writing
// as lambda (or anonymous)
x => x + 1
// as named
let addOne = x => x + 1
a syntax very similar to the one we use in Javascript/Typescript when declaring an arrow function. However, it is interesting to notice how no further annotation is required, while writing the same function in TypeScript will look like
const addOne = (x: number) => x + 1
which honestly is not a big a deal (not for such simple function anyway), but there's more to consider: the following code is perfectly fine at compile time, but it may be problematic to handle at runtime.
const addOne = (x: number) => x + 1;
console.log(addOne(parseInt("Dayum"))); // This prints out as "NaN"
On the other hand, writing the same code in ReScript
let addOne = x => x + 1
Js.log(addOne(Belt.Int.fromString("Dayum")))
will result in a type error
This has type: option<int>
Somewhere wanted: int
with the compiler warning us about the usage of a potentially unsafe cast operation, even if when dealing with an explicit one [1]. This will force us to properly handle all of the edge cases, something that I personally like [2].
Also, functions being expressions means that we can use them whenever
we can use an expression, for example we can mix
let
expressions and lambda
functions together
// Quiz time: What are resulting type and value for b?
let a = 5
let b = a + (val => val + 1)(5)
Once again, reading left to right may be useful to define type and
value of this last expression. Value
5
is passed as input to a lambda
function which adds to 1
its input argument, so
5 + 1 = 6
. Now, we just have to
add a
value, which is
5
to
6
.
So b
is
5 + 6 = 11
, and has type
int
.
3.2 Labeled Arguments
When dealing with multi-arguments functions, it may be useful to refer
them by name instead of relying on their position. This kind of
functionality is peculiar to ReScript and goes with the name of
labeled arguments.
In order to use labeled arguments, we just prefix each variable name
with the ~
symbol.
let difference = (~minuend, ~subtrahend) => minuend - subtrahend
// No matter the order, this will be evaluated as 5 - 4
let res = difference(~subtrahend = 4, ~minuend = 5);
Unfortunately in JavaScript/TypeScript we don't really have an equivalent syntax. However, we can emulate a similar behavior by using object literals
type DifferenceArgs = {
minuend: number;
subtrahend: number;
}
const difference = ({ minuend, subtrahend }: DifferenceArgs) => minuend - subtrahend;
const res = difference({ subtrahend: 4, minuend: 5 });
but again, the type system cannot infer the proper types so we have to
declare them explicitly, otherwise
minuend
and
subtrahend
will be evaluated as
any
, defying the goal of having
a type checker in place.
3.2.1 Optional labeled arguments
Labeled arguments may as well be defined as optional, with different syntaxes for:
- parameters that can be omitted;
- parameters that have a default value;
// Optional, with default value
let divide = (~divisor = 1, dividend) => dividend / divisor
let res = divide(5) // This will be evaluated as 5
// Optional, without default value
let getBreakfast = (~biscuits=?, drink) =>
if biscuits === Some(true) { `${drink} with biscuits` }
else { `${drink}` }
let breakfast = getBreakfast("Milk") // This will be evaluated as "Milk"
In the last example each function had only one optional
parameter. This is because ReScript has a formal rule about
optional arguments usage; in fact:
"an optional argument is considered intentionally omitted
when the 1st positional (i.e. neither labeled nor optional)
argument defined after it is passed in."
So basically, we should always have at least
one required positional argument in final position
or we get a warning from the compiler, unless all parameters are
optional in which case we're expected to use a final
()
.
Now, this is a really important note to keep in mind. Considering the following example
// All parameters are optional
let divide = (~divisor = 1, ~dividend=0, ()) => dividend / divisor
// So this will evaluate into 0
let resWithNoParams = divide()
// ...but what about this?
let resWithSingleParam = divide(~dividend=5)
what do we expect
resWithSingleParam
to evaluate
into? Let's figure that out in the next section.
3.3 Implicit currying
While still referring to our last example, we may be surprised that
resWithSingleParam
is
evaluated as
[Function: resWithSingleParam]
which means that is a function. Confused? Good, so was I.
This happens because
ReScript functions are curried by default
which is a fancy way to say that each function can take exactly one
argument. So writing
let fun = (x, y) => x + y
is
equivalent to
let fun = x => (y => x + y)
or, more in general
let fun = (arg1, arg2, ..., argN) => e
is semantically equivalent to
let fun = arg1 =>
(curr1(arg2) =>
(...
(currN(argN) => e)
)
)
This tell us that whenever we write a function that takes
n
parameters, we're just
writing a list of n functions and each
one of them takes exactly one argument and returns a function.
Having such a built-in mechanism makes the language really expressive and powerful, but comes with a big (potential) drawback: performances. Even if the compiler tries to optimize the transpiled code and remove unnecessary currying sometimes we just need to be sure that a certain function gets executed as uncurried in order to keep our code as performant as we can.
In such cases, we just put a dot before starting the parameter list in both function declaration and subsequent calls.
let uncurriedFn = (. arg1, ..., argN) => ...
// Note: both call and declaration requires the uncurry annotation.
let res = uncurried(. arg1, ..., argN)
It may be worth noting that uncurry notation and optional parameters are mutually exclusive, at least for now. Simply put, we either declare an uncurried function or we use optional parameters but any attempt to mix the two syntaxes will cause the compiler to throw the following error
Uncurried function doesn't support optional arguments yet
While the aforementioned considerations are something definitely to keep in mind and are effectively drawbacks, it's worth considering that we shouldn't really exceed with the number of parameters nor abusing optional parameters anyway. If anything, these limitations may help us in thinking a little bit more about the code we're writing instead of using a feature just because we are allowed to do so (I'm looking at you, object destructuring 🫵).
3.4 Let's wrap it up! 🫡
First thing first, thanks for reading through the whole article: I
really hope you enjoyed the content.
In this episode we saw that,
despite sharing a similar syntax, ReScript functions have their own
set of rules and features that set them apart from
JavaScript/TypeScript ones.
We're not done with functions yet,
but this episode was meant to provide a solid foundation about them.
In the incoming one we'll see some more advanced concepts, including a
tiny but incredibly useful operator to compose functions. If you're
curious and want to know more about it, then stick around for the
next episode. Up to then, happy coding and... see you next time! Cheers! 🤓
Amazing post! Thanks for it and keep it up!
ReplyDeleteThanks for the kind words and for reading! 🙏
Delete