Skip to content
Tech News
← Back to articles

A perfectable programming language

read original more articles
Why This Matters

This article highlights the significance of Lean as a 'perfectable' programming language, emphasizing its ability to incorporate formal properties and proofs directly into code. This approach enhances software reliability and correctness, pushing the boundaries of how programming languages can support rigorous verification. The concept underscores a future where programming languages seamlessly integrate theorem proving, benefiting both the tech industry and consumers through more dependable software.

Key Takeaways

a perfectable programming language

Alok Singh

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 := by a : Int ⊢ 6 = returnFive a + 1 unfold returnFive a : Int ⊢ 6 = 5 + 1 rfl 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 const ification. 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: inductive Player where | X | O deriving BEq , Inhabited inductive Square where | empty | occupied ( player : Player ) deriving BEq , Inhabited @[ simp ] def boardSize : Nat := 9 structure Board where squares : Array Square deriving BEq now the custom syntax: declare_syntax_cat tttCell syntax "X" : tttCell syntax "O" : tttCell syntax "_" : tttCell declare_syntax_cat tttRow syntax ( name := tttRowRule ) tttCell "|" tttCell "|" tttCell : tttRow declare_syntax_cat tttBoardSyntax syntax tttRow tttRow tttRow : tttBoardSyntax 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 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. theorem five_plus_one_eq_six : ∀ a : Int , returnFive a + 1 = 6 := by ⊢ ∀ ( a : Int ), returnFive a + 1 = 6 intro a a : Int ⊢ returnFive a + 1 = 6 unfold returnFive a : Int ⊢ 5 + 1 = 6 rfl 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?)