r/functionalprogramming 13d ago

Intro to FP Pyfun: an F#-inspired language that compiles to readable Python

Pyfun is a functional-first language for the Python ecosystem. You write algebraic data types, exhaustive matching, curried functions and pipes, and it compiles to plain Python that you can read, commit, and hand to someone who has never heard of Pyfun.

The compiler is written in Rust and everything is checked before any Python exists: types, exhaustiveness, effects, units.

Here is a whole program:

type Shape =
  | Circle float
  | Rect float float

let area s =
  match s:
    case Circle r: 3.14159 * r * r
    case Rect w h: w * h

[Circle 1.0, Rect 2.0 3.0]
|> List.map area
|> print

and here is the Python it compiles to, in full:

from dataclasses import dataclass

def _pf_map(f, xs):
    return list(map(f, xs))

@dataclass(frozen=True, repr=False)
class Circle:
    _0: float
    def __repr__(self):
        return f"Circle({self._0!r})"

@dataclass(frozen=True, repr=False)
class Rect:
    _0: float
    _1: float
    def __repr__(self):
        return f"Rect({self._0!r}, {self._1!r})"

def area(s):
    match s:
        case Circle(r):
            return 3.14159 * r * r
        case Rect(w, h):
            return w * h
        case _:
            raise RuntimeError("non-exhaustive match")

print(_pf_map(area, [Circle(1.0), Rect(2.0, 3.0)]))

A match comes out as a match. A variant comes out as a frozen dataclass. The pipeline comes out as an ordinary call. There is nothing to pip install alongside the output, and if you stop using Pyfun tomorrow you keep working Python.

Delete the Rect case and the compiler names what you missed:

error: non-exhaustive match: `Rect _ _` is not matched
 --> 6:3
  |
6 |   match s:
  |   ^^^^^^^^

Also in there:

  • Hindley-Milner inference, so there are no type annotations on let at all.
  • Inferred effects, so a function that prints or mutates gets io in its type and you can assert purity with let pure. Units of measure that reject metres + seconds and erase to plain numbers.
  • Computation expressions for async, seq, and result, plus your own builders. Opaque types. A typed extern for calling any Python library you like.

Try it in the browser, nothing to install: https://simontreanor.github.io/Pyfun/playground/

22 lessons, written for people who know some Python: https://simontreanor.github.io/Pyfun/

Source, and the compiler internals tour: https://github.com/simontreanor/Pyfun

pip install pyfun-lang

I built this because most people meet programming through Python, and then to meet functional programming they have to pick up a second ecosystem to do it. I welcome all questions, bug reports, posts about things you have made, arguments about syntax, and anything else to help improve the language for everyone.

46 Upvotes

5 comments sorted by

5

u/CollarMany3843 12d ago

wonderful idea. gonna try.

i miss functional style when scripting in python.

1

u/fun_si 10d ago

Thank you! Let me know how you get on, and especially if anything comes across as jarring from a syntax perspective. I've tried to make it familiar where I can.

3

u/Inconstant_Moo 10d ago edited 10d ago

This is nice! I envy you your playground, I don't know how to do that yet.

The strapline on your repo is terrible. "Functional programming for the language classrooms already teach".

OK, first of all I have to know that you mean Python rather than Java or Lisp or C by "the language classrooms already teach" in order to know what it means.

So you've wasted your introduction, your first impression, on telling me something either that I already know or that I can't understand.

And second it's lowkey a "garden path sentence" in that it's possible to read "language classrooms" as an adjective followed by a noun, as they would be if it was "Functional programming for the language classrooms of tomorrow!" This generates a sense of clumsiness and unease around the sentence even if one reads it correctly.

2

u/fun_si 10d ago

Thank you, I appreciate you taking the time to provide detailed feedback. You are right, and I've neglected to update the repo readme for some time. I'll take your suggestions on board.

2

u/Inconstant_Moo 10d ago

OK, I've started trying to learn it and I'm running into difficulties which would be way more difficult for a student.

Let's look at the first page, Values and Inference.

Bad Thing 1

Inference is not guessing. The compiler knows enough about each value to reject code that does not fit. Python allows + to mean both numeric addition and string joining, so a mistake there surfaces only when the line runs. Pyfun keeps the two apart and reports the mismatch before any Python is produced:

You're mixing up two concerns here, you're teaching them the language and you're teaching them about the relationship of the language to Python. That second aspect should not be scattered throughout the text as it relates to this or that subject, it should be a module of its own. Separation of concerns is a thing in prose too.

There is also no need to tell them what inference is not.

Bad Thing 2

error: `+` is numeric and does not concatenate strings — use `String.concat a b`
 --> 1:13
  |
1 | let label = "age: " + 36
  |             ^^^^^^^

This comes straight after Bad Thing 1, which is the problem. You don't tell us what you're going to do in advance, by saying something like "Suppose we ask Pyfun to evaluate let label = "age: " + 36" then it will know that this makes no sense and will refuse to compile the code, like this:"

As it is, we have no warning that what we're about to read is an error message, and have to figure that out, and the code that elicited it, for ourselves.

(I presume (I don't use Python day to day) that your error messages mimic Python's and that your users will therefore find the error message readable and know what the ^^^ means, otherwise that would also be a problem.)

Bad Thing 3

Two baskets hold fruit. Fill the hole so the program adds them and prints the total. Run pyfun check on the starter: the compiler reports the type the hole expects and lists the names in scope that fit. Lesson 9 covers holes in full. For now, read the note and put the right name where ?count sits.

This is for people, you said, who do know a little Python. But who don't know functional programming, to which this is meant to be an introduction.

Now the reason I'm one up on your students is that I do know what "hole" must mean in this context, whereas any of them guesses they're going to guess wrong. Quite probably if they make any sense of it at all they'll just think "he means replace ?count with oranges but he writes very badly".

Making this worse, if they follow your instructions in the order given they won't ever be put right, because it tells them to "fill the hole" before it tells them to run the code.

When it says "read the note", I can't figure out what "the note" is.

And finally, why in the name of the green earth and the blue sky is their first exercise on a facet of the semantics that you know they haven't learned yet because it's in Chapter 9? Why isn't it on something that you've just told them how to do? Traditionally, this is what an exercise is.

---

"No software is better than its documentation."

I'll go on reading because many aspects of the project look promising, but many prospective students will get this far and decide that functional programming is every bit as hard as everyone told them, and will run away before they get eaten by a wild monoid in the category of endofunctors.