Yield it all!
You thought the vertical code was just for IO
? You were wrong! Vertical code (the one that spans vertically and can be read top to bottom) is quite universal in Funky. In this part, we'll see how it can be applied to lists and their generation.
The yield
function
The basic list constructor, the double colon, is an infix function. That makes it unusable for vertical code. However, the standard library defines a synonym of ::
called yield
:
func yield : a -> List a -> List a = (::)
It's a literal synonym. But, it's a prefix function. How does that help?
Take this list:
[1, 2, 3]
Rewrite it with the colons:
1 :: 2 :: 3 :: empty
Cool. Now, how would this look if we used yield
instead?
yield 1 (yield 2 (yield 3 empty))
All we did was replace all ::
s with yield
s. And here it comes. Can you see it? We can use the semicolon and make it vertical!
yield 1;
yield 2;
yield 3;
empty
Beautiful!
To get to know yield
a little better, let's use it to rewrite some well-known functions. How would the map
function look like using yield
? Something like this:
func my-map : (a -> b) -> List a -> List b =
\f \list
for list (
\x \next
yield (f x);
next
);
empty
The for
function isn't just for IO
. It can be used with anything that composes vertically. We take the function f
and the list
and we yield
all of its elements in order, except we transform them with f
.
We can even make the code shorter with function composition:
func my-map : (a -> b) -> List a -> List b =
\f \list
for list (yield . f);
empty
That's really nice.
The when
function
How would filter
look like with yield
? In the case of filter
, we can't yield
all the elements of the list. We must only yield
those that satisfy the predicate. That's where the when
function comes handy:
$ funkycmd -types
> when
Bool -> (a -> a) -> a -> a
It's kinda similar to if
, but different. While if
chooses between two alternatives, when
conditionally applies its second argument (the body) to the third (the next). Specifically, when
works like this:
when true body next # => body next
when false body next # => next
For example:
when (x > 7) (1 +) x
This expression evaluates to x + 1
if x
is more than 7, otherwise, it evaluates to x
.
So, let's use when
to implement my-filter
!
func my-filter : (a -> Bool) -> List a -> List a =
\p \list
for list (
\x \next
when (p x) (yield x);
next
);
empty
That's it! Here, we can't easily compress the loop body into some stunning composition, but so what. It isn't that bad anyway.
Formatting lists
Structuring list generation vertically comes very handily when dealing with complex list generating functions. One of the best examples is a function to format lists so that we can print them with one println
. Such a function isn't currently present in the standard library. But we'll make our own!
First, we gotta choose the type. What could it be? Perhaps this one?
List a -> String
Well, this is what it will be in the future when the with
clause gets implemented, but so far, we can't do it that way. Why? We're accepting List a
: a list of any type. To convert it to a string, we need to convert each of its elements to a string. But, there's no general way of converting an arbitrary type to string - the string
function is only implemented for a handful of types.
This will do:
(a -> String) -> List a -> String
In addition to the list itself, we'll accept a function that helps us convert the elements to strings. Okay, let's get to the code!
func list->string : (a -> String) -> List a -> String =
\str \list
# TODO
The type String
is an alias for List Char
- the list of characters. That's what we have to produce. We start with the opening square bracket:
func list->string : (a -> String) -> List a -> String =
\str \list
yield '[';
# TODO
Now we have to loop over the list and yield the string versions of the elements. We also gotta put commas between them. We gotta put a comma before each element except the first one. How are we going to achieve that?
We'll learn two more functions: enumerate
and for-pair
. The enumerate
function takes a list and adds an index to each element. Its type is List a -> List (Pair Int a)
. For example: enumerate ["A", "B", C"]
evaluates to [pair 0 "A", pair 1 "B", pair 2 "C"]
. You get the idea. Now, for-pair
lets us easily iterate over a list of pairs. It's just like for
, but it's body function takes two elements instead of one - for-pair
unpacks the pairs.
func list->string : (a -> String) -> List a -> String =
\str \list
yield '[';
for-pair (enumerate list) (
\i \x \next
# TODO
);
# TODO
The i
variable is the index, the x
variable is the original element from list
. If the index is zero, we know we're dealing with the first element and we won't add the comma. Otherwise, we'll add it.
func list->string : (a -> String) -> List a -> String =
\str \list
yield '[';
for-pair (enumerate list) (
\i \x \next
when (i > 0) (yield-all ", ");
yield-all (str x);
next
);
# TODO
What's that yield-all
? It's just like yield
, but it yields a whole list of things, instead of just one element. It's a synonym for ++
. In general:
yield-all list;
next
is equivalent to:
for list yield;
next
All that remains now is finishing with the closing square bracket:
func list->string : (a -> String) -> List a -> String =
\str \list
yield '[';
for-pair (enumerate list) (
\i \x \next
when (i > 0) (yield-all ", ");
yield-all (str x);
next
);
yield ']';
empty
And we're done! It's so nice and readable having this function expressed vertically.
Note. Actually,
list->string
can be done nicer in a functional style using thejoin
function.
"[" ++ join ", " (map str list) ++ "]"
Functional solutions are often superior to imperative ones. We've done
list->string
imperatively just for the demonstration.
Let's test it out! We'll print all the primes until 100. First, the prime testing function:
func prime? : Int -> Bool =
\n
all (\x not zero?; n % x) (range 2 (n - 1))
It simply tests whether no number between 2 and n-1 divides n. The %
sign is the modulo operator. Now we'll make an infinite list of primes:
func primes : List Int =
filter prime? (iterate (+ 1) 2)
Details. The function
iterate
successively applies the provided function to the provided initial value producing an infinite list of all the results. In our case,iterate (+ 1) 2
produces[2, 3, 4, 5, 6, ...]
. For example,iterate (* 2) 1
would produce[1, 2, 4, 8, 16, 32, ...]
.
Okay, now, let's print them up to 100:
func main : IO =
let (take-while (< 100) primes) \primes<100
println (list->string string primes<100);
quit
Details. The
take-while
function is different fromfilter
. Whilefilter
takes all the elements that satisfy a predicate,take-while
takes elements from the list, but stops taking them the moment it encounters the first element to not satisfy the predicate. That's important here. Had we usedfilter
, the program wouldn't halt. It would continue searching the infinite list of primes for another element less than 100, but it would never find one. Usingtake-while
makes the program halt because we never search beyond the first wrong one.
Let's run it!
$ funkycmd primes.fn
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]
Works like charm!
In the next part, we'll learn how to express state flow. Things will get really imperative... in a purely functional way.