100 Languages Speedrun: Episode 82: Pyret
Tags: pyret
Published: 2022-02-08 18:23:02

Let's talk education. If you want to learn programming without spending years on it, there are basically three decent ways that are proven to work:

* you start with Ruby to learn it properly in the best language, with proper TDD and so on (what the best bootcamps are doing)
* you start with HTML+CSS, then learn JavaScript (frontend route)
* you start with Python, in Jupyter style environment (data science route)

Most other languages are terrible for beginners, due to enormous amount of additional complexity that gets in the way of learning. Yes even the "good" languages. That's not to say Ruby, JavaScript, and Python aren't complex - but in their case complexity is largely opt-in, and beginners can get to employable levels of proficiency without ever creating a Ruby DSL, Python decorator, JavaScript prototype chain, or such.

There are a few more languages that are approachable for beginners (SQL, Spreadsheets, Logo), but they offer no easy path to real world development. All your elite Excel skills won't transfer into programming skills well.

Universities teach programming all wrong. They start with an extremely beginner-hostile language like C, Java, SML, Scheme, or whatnot, and the only reason programmers come out by the end is that they have years to do that.

So anyway, [Pyret is a new programming language](https://www.pyret.org/index.html) designed specifically for education, that tries to be far more beginner-friendly than any existing language, while teaching transferable skills. Let's see how it worked.

### Hello, World!

We can start by writing a Hello, World!, and the language already fails, by printing a lot of irrelevant crap in addition to our message:

```ruby
#!/usr/bin/env pyret

print("Hello, World!")
```

```
$ ./hello.arr
1/1 modules compiled (hello.arr)
Cleaning up and generating standalone...
Hello, World!
The program didn't define any tests.
```

We need to pass a bunch of flags (`-q` for quiet, `-k` for no tests) just to get hello world working:

```ruby
#!/usr/bin/env pyret -qk

print("Hello, World!\n")
```

```
$ ./hello2.arr
Hello, World!
```

### Fibonacci

Let's do the Fibonacci sequence!

```ruby
#!/usr/bin/env pyret -q

fun fib(n):
  if n <= 2:
    1
  else:
    fib(n - 2) + fib(n - 1)
  end
end
check:
  fib(1) is 1
  fib(2) is 1
  fib(10) is 55
end
```

This part isn't too bad. `fib` definition is fine, in some hybrid Ruby-Python syntax.

Pyret wants unit tests to follow the code with `check:` block. In real apps, I don't like this mixing of app code and test code, but this is a reasonable thing to do in language for beginners. Pyret uses `==` for comparisons in normal program, but in tests it's `is`, which seems like unnecessary complexity 🤷.

Thing go downhill fast when we try to print it, in the same format I always use.:

```
for each(n from range(1, 21)):
  block:
    print("fib(")
    print(n)
    print(")=")
    print(fib(n))
    print("\n")
  end
end
```

WTF is this? Pyret has no string interpolation, which is an insane obstacle to throw at beginners. There are no easy workarounds like passing multiple arguments to print, or automatically turning numbers into strings with `"fib(" + n`. Oh and if you have multiple expressions in a block, you need to wrap the whole thing in a pointless `block:` block. WTF?

OK, let's try another approach, how about we construct the string once, without interpolating?

```
for each(n from range(1, 21)):
  print("fib(" + to-string(n) + ")=" + to-string(fib(n)) + "\n")
end
```

This is far less readable than string interpolation would be:

```ruby
# Not Pyret
for each(n from range(1, 21)):
  print("fib(#{n})=#{fib(n)\n")
end
```

And of course it even prints some crap with `-q`, instead of doing the reasonable thin and only printing extra stuff when there are problems:

```
$ ./fib2.arr
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
fib(11)=89
fib(12)=144
fib(13)=233
fib(14)=377
fib(15)=610
fib(16)=987
fib(17)=1597
fib(18)=2584
fib(19)=4181
fib(20)=6765
Looks shipshape, all 3 tests passed, mate!
```

You can disable tests with `-k`, but then you won't know if there are any issues. Overall, it's baffling design all over.

### FizzBuzz

Here's one way to do the FizzBuzz. I'm converting `n` to string inside `fizzbuzz` function, as printing is otherwise such pain. Pyret doesn't have `println` like vast majority of languages, and you can't `print(n, "\n")` or `print(n + "\n")` or even just have two `print` without wrapping the whole thing in a `block: ... end`.

```ruby
#!/usr/bin/env pyret -q

fun fizzbuzz(n):
  if num-modulo(n, 15) == 0:
    "FizzBuzz"
  else if num-modulo(n, 5) == 0:
    "Buzz"
  else if num-modulo(n, 3) == 0:
    "Fizz"
  else:
    to-string(n)
  end
end
check:
  fizzbuzz(30) is "FizzBuzz"
  fizzbuzz(31) is "31"
  fizzbuzz(33) is "Fizz"
  fizzbuzz(35) is "Buzz"
end

for each(n from range(1, 101)):
  print(fizzbuzz(n) + "\n")
end
```

And again, there's an extra stupid line at the end:

```
$ ./fizzbuzz.arr
1
2
Fizz
4
Buzz
...
Fizz
97
98
Fizz
Buzz
Looks shipshape, all 4 tests passed, mate!
```

### Defining a class

Let's define a `Person` class! Oh wait, we can't. Pyret doesn't have classes, it only has ML-style data types:

```ruby
#!/usr/bin/env pyret -q

data Person:
  person(first-name, last-name)
end
check:
  to-string(person("Alice", "Smith")) is "person(Alice, Smith)"
end

people = [list:
  person("Alice", "Wonderland"),
  person("Bob", "Marley"),
  person("Eve", "Jackson"),
]

for each(a-person from people):
  print(to-string(a-person) + "\n")
end
```

```
$ ./person.arr
person(Alice, Wonderland)
person(Bob, Marley)
person(Eve, Jackson)
Looks shipshape, your test passed, mate!
```

The logic of this choice is - why teach people OOP which pretty much every modern language is based on to some degree, when you can teach them dead 1970s' approach to programming, right?

### Operator Precedence

OK, just one more thing, can Pyret at least do basic math:

```ruby
#!/usr/bin/env pyret -q

a = 20
b = 4
c = 100

print(a + b * c)
```

Of course it cannot:

```
$ ./math.arr
Operators of different kinds cannot be mixed at the same level, but `+` is at math.arr:7:8-7:9 at the same level as `*` at math.arr:7:12-7:13. Use parentheses to group the operations and to make the order of operations clear.
There were compilation errors
```

Because obviously an average programming student never had a single math class.

By the way Pyret is not the only language which does this. Smalltalk-derived Self language does, because Smalltalk had an insane idea of not having operator precedence (2 + 3 * 4 means 20 in Smalltalk), so to dig itself out of this insane hole, Self forced parentheses in this situation. This way any Smalltalk code ported to Self, which didn't obey the usual rules, would need some extra parentheses one way or the other. Better explicit than broken. Pyret has zero reason to do this.

### Should you use Pyret?

No. The language is terrible, and completely unsuitable for education, or for any other use. It's basically ML with nicer syntax, and nobody needs that.

One big area where languages differ a lot, and where a beginner friendly language could make a big difference is error messages. Experienced programmers can deal with cryptic errors, beginners really can't. Pyret fails at this as well, and its error messages are generally just awful.

The language is bad, and you should use Ruby, JavaScript, or Python for teaching beginners. Absolutely not this.

### Code

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

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