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
$ ./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:
#!/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:
#!/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 lastaddpattern
, 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))
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
Step by step:
- Coconut has lazy lists everywhere, so we'll use some of those
-
count(1)
is an infinite lazy list1, 2, 3, ...
-
(| ... |)
is lazy list syntax, just like[...]
is normal list syntax -
::
concatenates lazy lists, so in principle we want to dofizz = (| "", "", "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()
andbuzz()
intofizzbuzz
infinite stream of tuples("", ""), ("", ""), ("Fizz", ""), ("", ""), ("", "Buzz"), ...
- we then
map$("".join)
to get infinite stream of strings like"", "", "Fizz", "", "Buzz", "Fizz", ...
(every 15th beingFizzBuzz
) - 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 ofprint(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.
Top comments (0)