DEV Community

Tomasz Wegrzanowski
Tomasz Wegrzanowski

Posted on • Edited on

100 Languages Speedrun: Episode 93: Coconut

Coconut 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.

#!/usr/bin/env coconut -qr

"Hello, World!" |> print
Enter fullscreen mode Exit fullscreen mode
$ ./hello.coco
Hello, World!
Enter fullscreen mode Exit fullscreen mode

"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:

#!/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
Enter fullscreen mode Exit fullscreen mode
$ ./fib.coco
1
1
2
3
5
8
13
21
34
55
...
Enter fullscreen mode Exit fullscreen mode

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:

#!/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
Enter fullscreen mode Exit fullscreen mode
$ ./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
...
Enter fullscreen mode Exit fullscreen mode

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:

@_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))
Enter fullscreen mode Exit fullscreen mode

FizzBuzz

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

#!/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
Enter fullscreen mode Exit fullscreen mode

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.

Code for the Coconut episode is available here.

Top comments (0)