a perfectable programming language

at a party, Sydney Von Arx asked if i could name 40 programming languages. yeah, that's the bay for you. racket, agda, clean, elm, typescript, sh, ASP, verilog, javascript, scheme, rust, nim, intercal, sed, isabelle, visual basic, zsh, alokscript, coq, idris, hack, prolog, whitespace, purescript, go, odin, haskell, python, tcsh, unison, clingo, bash, java, zig, cyclone, php, awk, c, actionscript, c++.

But Lean is the best.

why?

because it's perfectable. it's not perfect, but it is perfectable. you can write down properties about Lean, in Lean.

the whole edifice of these facts and properties shall be known as progress.

in every language, you eventually wanna say stuff about the code itself.

like here's a function that always returns 5, but in almost no language can you really use that fact in a way that the language itself helps you with.

function returnFive(x : number) : number {
  return 5;
}
def returnFive (unused variable `x` Note: This linter can be disabled with `set_option linter.unusedVariables false`x : Int) : Int := 5 theorem returnFive_eq_five (x : Int) : returnFive x = 5 := rfl example (a : Int) : 6 = returnFive a + 1 := a:IntโŠข 6 = returnFive a + 1 a:IntโŠข 6 = 5 + 1 All goals completed! ๐Ÿ™

languages without types tend to grow them, like PHP in 7.4 and Python type annotations, and the general trend towards TypeScript and Rust.

inevitably, people want to push types. even Go. C++ templates are the ultimate example. if it can be computed at compile time, at some point someone wants to, like Rust's ongoing constification.

but the easiest way to do anything is properly. doing it properly is basically dependent types. there are fancier things than them, but like Turing-completeness, dependent types can get you there. hence perfectable.

on top of the types, you want infrastructure for showing 2 types are equal/not equal. this is basically a theorem prover. any dependent language can become a theorem prover, but it needs to grow the nice API we call "theorem proving infrastructure".

that's half the story. the semantics half. the syntax half is metaprogramming and custom syntax.

metaprogramming

most languages have no facility for this, or it's a bit awkward, like Rust's procedural macros.

Lean is freakishly seamless. here's tic-tac-toe with a custom board notation:

/-- The two players in tic-tac-toe. -/ inductive Player where | X | O deriving BEq, Inhabited /-- A square on the board: either empty or occupied by a player. -/ inductive Square where /-- Nobody has played here yet. -/ | empty /-- A player has claimed this square. -/ | occupied (player : Player) deriving BEq, Inhabited @[simp] def boardSize : Nat := 9 /-- A 3x3 tic-tac-toe board. -/ structure Board where squares : Array Square deriving BEq

now the custom syntax:

/-- A cell in the board literal: `X`, `O`, or `_` (empty). -/ declare_syntax_cat tttCell syntax "X" : tttCell syntax "O" : tttCell syntax "_" : tttCell /-- A row of three cells separated by `|`. -/ declare_syntax_cat tttRow syntax (name := tttRowRule) tttCell "|" tttCell "|" tttCell : tttRow /-- Three rows make a complete 3x3 board. -/ declare_syntax_cat tttBoardSyntax syntax tttRow tttRow tttRow : tttBoardSyntax /-- Elaborate a single cell into a `Square`. -/ private def elabTttCell (stx : Lean.Syntax) : Lean.Elab.Term.TermElabM Square := match stx with | `(tttCell| X) => pure (.occupied .X) | `(tttCell| O) => pure (.occupied .O) | `(tttCell| _) => pure .empty | _ => Lean.throwError s!"unsupported cell syntax {stx}" open Lean Elab Term in /-- `board!` turns a visual board layout into a `Board` at compile time. Each cell is validated during elaboration. -/ elab "board! " b:tttBoardSyntax : term => do let mut squares : Array Square := #[] unless b.raw.getNumArgs = 3 do Lean.throwError s!"Expected 3 rows, got {b.raw.getNumArgs}" for rowIdx in [:3] do let row := b.raw.getArg rowIdx unless row.isOfKind `tttRowRule do Lean.throwError s!"malformed tttRow" let cells := #[row.getArg 0, row.getArg 2, row.getArg 4] for cell in cells do squares := squares.push (โ† elabTttCell cell) unless squares.size = boardSize do Lean.throwError s!"internal error: expected 9 squares, got {squares.size}" let squareTerms โ† squares.mapM fun sq => match sq with | .empty => `(Square.empty) | .occupied .X => `(Square.occupied Player.X) | .occupied .O => `(Square.occupied Player.O) let arrSyntax โ† `(#[$squareTerms,*]) let boardTerm โ† `(Board.mk $arrSyntax) Lean.Elab.Term.elabTerm boardTerm none X | O | _ _ | X | _ O | _ | X#eval board! X | O | _ _ | X | _ O | _ | X Win X#eval getGameStatus (board! X | X | X O | O | _ _ | _ | _)
X | O | _
_ | X | _
O | _ | X
Win X

this lets you design APIs in layers and hide them behind syntax. plus the interpretation of the syntax can be swapped easily. Lean's type system helps a bit with the metaprogramming (but i would love to see meta-metaprogramming with some sort of modality for better infra around Lean Syntax).

def ยซ๐ŸŽฎ tic tac toe ๐ŸŽฎยป : Board := board! X | O | X O | X | O X | _ | O Win X#eval getGameStatus ยซ๐ŸŽฎ tic tac toe ๐ŸŽฎยป
Win X

doing this properly just is a theorem prover. theorem proving comes about from convergent evolution in programming.

speed

this is the biggest one. slow languages suck. why use a computer then.

Lean could be faster. it's not Rust-fast, but it has a very high ceiling for optimization thanks to the ability to show 2 pieces of code equal.

/-- If two functions are provably equal on all inputs, the compiler can freely substitute one for the other. -/ theorem five_plus_one_eq_six : โˆ€ a : Int, returnFive a + 1 = 6 := โŠข โˆ€ (a : Int), returnFive a + 1 = 6 a:IntโŠข returnFive a + 1 = 6 a:IntโŠข 5 + 1 = 6 All goals completed! ๐Ÿ™

Leo de Moura seems convinced of the need for this too, enough that backward compat is going by the wayside. thankfully in AI world rewriting code is a lot easier, and a theorem prover is the ultimate refactoring tool (how could it not be?)

community

Lean is the only language in its class that's actually gaining traction. Coq, Idris, Agda โ€” none of them are really competing anymore. Idris would otherwise count as a real programming language that's also a prover, but the community never reached critical mass. F* could count too, but its community is a rounding error. Lean is the one with raw programming ability that's also a theorem prover, and it's growing.

this blog post is itself Lean code.