100 Languages Speedrun: Episode 93: Coconut
Tags: python, coconut
Published: 2022-02-17 01:20:35

[Coconut](http://coconut-lang.org/) is just Python with some extra syntax.

All normal Python code should generally run in Coconut as well, so I'll just focus on the new stuff.

### Hello, World!

Coconut can be installed with `pip3 install coconut`

Of course the biggest syntax hit these days is `|>` pipeline operator, so that's where we will start!

Coconut normally compiles to Python instead of just running, so we need to pass a few arguments to it. It's all cached, but running it the first time takes over 3s, which is honestly very slow even for a compiled language.

```python
#!/usr/bin/env coconut -qr

"Hello, World!" |> print
```

```
$ ./hello.coco
Hello, World!
```

`"Hello, World!" |> print` is just `print("Hello, World!")`, and of course for something as simple it's not really necessary.

### Fibonacci

Coconut also provides pattern matching, which we can use for the simple case like this:

```python
#!/usr/bin/env coconut -qr

from functools import cache

@cache
def fib(n):
  case n:
    match 1:
      return 1
    match 2:
      return 1
    match _:
      return fib(n-1) + fib(n-2)

for i in range(1, 101):
  i |> fib |> print
```

```
$ ./fib.coco
1
1
2
3
5
8
13
21
34
55
...
```

`functools.cache` is part of regular Python. The Coconut specific parts are `case/match` (`_` matches everything), and some more `|>` pipelines. `i |> fib |> print` is just `print(fib(i))`

That's all nice, but it just prints numbers not the template we want, and isn't very "functional":

### Very Fancy Fibonacci

Let's try something much fancier:

```python
#!/usr/bin/env coconut -qr

from functools import cache

def fib(1) = 1
addpattern def fib(2) = 1
@cache
addpattern def fib(n) = fib(n-1) + fib(n-2)

range(1, 101) |> map$(n -> f"fib({n}) = {fib(n)}") |> "\n".join |> print
```

```
$ ./fib2.coco
fib(1) = 1
fib(2) = 1
fib(3) = 2
fib(4) = 3
fib(5) = 5
fib(6) = 8
fib(7) = 13
fib(8) = 21
fib(9) = 34
fib(10) = 55
...
```

There's a lot going on here:

* like in quite a few functional languages we can define function with pattern matching. Syntax is quite awkward with extra `addpattern` because it needs to somehow get squeezed into Python compatible syntax
* due to the way it's implemented, decorators like `cache` must go on the last `addpattern`, even thought it would look much better to put it first
* `map$(...)` is lazy, nothing actually gets evaluated at this point
* `->` is lambda syntax
* annoyingly sending lazy iterator to `print` doesn't evaluate it, so we need to convert it somehow
* `"\n".join` forces evaluation - and joins the elements with newlines (`print` adds extra newline at the end)

This `fib` compiles to this block of code, achieving this in plain Python is not trivial:

```python
@_coconut_mark_as_match
def fib(*_coconut_match_args, **_coconut_match_kwargs):
    _coconut_match_check_0 = False
    _coconut_FunctionMatchError = _coconut_get_function_match_error()
    if (_coconut.len(_coconut_match_args) == 1) and (_coconut_match_args[0] == 1):
        if not _coconut_match_kwargs:
            _coconut_match_check_0 = True
    if not _coconut_match_check_0:
        raise _coconut_FunctionMatchError('def fib(1) = 1', _coconut_match_args)

    return (1)
@_coconut_addpattern(fib)
@_coconut_mark_as_match
def fib(*_coconut_match_args, **_coconut_match_kwargs):
    _coconut_match_check_1 = False
    _coconut_FunctionMatchError = _coconut_get_function_match_error()
    if (_coconut.len(_coconut_match_args) == 1) and (_coconut_match_args[0] == 2):
        if not _coconut_match_kwargs:
            _coconut_match_check_1 = True
    if not _coconut_match_check_1:
        raise _coconut_FunctionMatchError('addpattern def fib(2) = 1', _coconut_match_args)

    return (1)
@cache
@_coconut_addpattern(fib)
@_coconut_mark_as_match
def fib(*_coconut_match_args, **_coconut_match_kwargs):
    _coconut_match_check_2 = False
    _coconut_match_set_name_n = _coconut_sentinel
    _coconut_FunctionMatchError = _coconut_get_function_match_error()
    if (_coconut.len(_coconut_match_args) <= 1) and (_coconut.sum((_coconut.len(_coconut_match_args) > 0, "n" in _coconut_match_kwargs)) == 1):
        _coconut_match_temp_0 = _coconut_match_args[0] if _coconut.len(_coconut_match_args) > 0 else _coconut_match_kwargs.pop("n")
        if not _coconut_match_kwargs:
            _coconut_match_set_name_n = _coconut_match_temp_0
            _coconut_match_check_2 = True
    if _coconut_match_check_2:
        if _coconut_match_set_name_n is not _coconut_sentinel:
            n = _coconut_match_temp_0
    if not _coconut_match_check_2:
        raise _coconut_FunctionMatchError('addpattern def fib(n) = fib(n-1) + fib(n-2)', _coconut_match_args)

    return (fib(n - 1) + fib(n - 2))
```

### FizzBuzz

The usual Python FizzBuzz works as well, but that's not the point, let's do this in very functional way:

```python
#!/usr/bin/env coconut -qr

nums = count(1)

@recursive_iterator
def fizz() = (| "", "", "Fizz" |) :: fizz()

@recursive_iterator
def buzz() = (| "", "", "", "", "Buzz" |) :: buzz()

fizzbuzz = zip(fizz(), buzz()) |> map$("".join)
fizzbuzznum = zip(fizzbuzz, nums) |> map$(x -> x[0] or x[1])

fizzbuzznum$[:100] |> map$print |> consume
```

Step by step:

* Coconut has lazy lists everywhere, so we'll use some of those
* `count(1)` is an infinite lazy list `1, 2, 3, ...`
* `(| ... |)` is lazy list syntax, just like `[...]` is normal list syntax
* `::` concatenates lazy lists, so in principle we want to do `fizz = (| "", "", "Fizz" |) :: fizz` - this unfortunately segmentation faults Python
* so we need to work around this by wrapping it in a function and adding `@recursive_iterator` decorator
* `fizz()` returns infinite lazy list that goes on `"", "", "Fizz", "", "", "Fizz", "", "", "Fizz", ...`
* `buzz()` returns infinite lazy list that goes on `"", "", "", "", "Buzz", "", "", "", "", "Buzz", ...`
* we zip `fizz()` and `buzz()` into `fizzbuzz` infinite stream of tuples `("", ""), ("", ""), ("Fizz", ""), ("", ""), ("", "Buzz"), ...`
* we then `map$("".join)` to get infinite stream of strings like `"", "", "Fizz", "", "Buzz", "Fizz", ...` (every 15th being `FizzBuzz`)
* we then zip that again with `nums` to get infinite stream of tuples like `("", 1), ("", 2), ("Fizz", 3), ("", 4), ("Buzz", 5), ...`
* then `map$(x -> x[0] or x[1])` turns it into proper FizzBuzz infinite stream
* finally we chop the first 100 elements of the infinite stream with `fizzbuzznum$[:100]`
* then we lazy map it to `print`, so we have lazy list of `print(1), print(2), print("Fizz"), print(4), print("Buzz"), ...` - but nothing is printed yet!
* `consume` forces that list to be evaluated, executing all the print functions

Is it more intuitive that Python? Obviously not.

### Should you use Coconut?

Not for anything serious.

Python doesn't really need more syntax. Python's biggest syntax pain point was lack of string interpolation, and after resisting for far too long it finally caved in and added the `f`-strings. It made sense to create CoffeeScript for pre-ES6 JavaScript, or MoonScript for Lua. There's not much point doing this for Python.

Still, Coconut is a fun language to play with for a weekend. You get access to all the Python syntax and library, so you can ease into more functional style in a supportive environment, nobody's throwing you into deep water. It's a lot less approachable than Python, but it's so much easier than Haskell, with its really complicated type system, monads, and no escape hatches. With Coconut you can just play a bit with sprinkling a bit of functional pixie dust over your Python code - and if something is too difficult, you can do that part in plain Python.

The downside of doing things this way, is that you'll likely get really bad error messages, some coming from Coconut dealing with the syntax, some from Python running compiled code. If you try a standalone language, there's some hope for more meaningful error messages, but that's not really a guarantee.

Anyway, if any Coconut devs are reading it, please fix Coconut VSCode extension. Cmd-/ adding `//` comments (which don't work) instead of `#` comment makes it pretty much unusable, and I had to switch back to Python extension. Small things like that are super annoying and it's like one line somewhere.

### Code

[All code examples for the series will be in this repository](https://github.com/taw/100-languages-speedrun).

[Code for the Coconut episode is available here](https://github.com/taw/100-languages-speedrun/tree/master/episode-93-coconut).