Zulip Chat Archive
Stream: lean4
Topic: Trying to write an interpreter function over HList of Exp
James Good (Aug 15 2024 at 04:18):
Hello,
I was working through a compiler book that uses Racket.
However, I don't fancy dynamic types, so I wanted to see if I could implement the designs they present in the book in a dependently typed language.
I started off using Idris2 awhile back, but I've switched to learning Lean recently and attempted to port over what little I had started on.
I can't get the interpreter function in Lean to compile, however, it compiles and runs just fine in Idris2.
Here's the Idris2 code:
module MinInterp
import Data.String
------------------------------------------------------------
-- Define Expression Types
record IntExp where
constructor MkIntExp
x : Int
record BoolExp where
constructor MkBoolExp
x : Bool
record StringExp where
constructor MkStringExp
x : String
-----------------------------------------------------------
-- Define `HList`
data HList : List Type -> Type where
HNil : HList Nil
(:::) : {t : Type} -> t -> HList ts -> HList (t :: ts)
infixr 5 :::
-----------------------------------------------------------
-- Define Primitive Operations
record Prim (e : List Type) where
constructor MkPrim
op : String
args : HList e
-- Example instances
eight : IntExp
eight = MkIntExp 8
neg_eight : Prim [IntExp]
neg_eight = MkPrim "-" (eight ::: HNil)
rd : Prim Nil
rd = MkPrim "read" HNil
----------------------------------------------
-- Define Result Type
data Result : Type where
IntResult : Int -> Result
BoolResult : Bool -> Result
StringResult : String -> Result
Show Result where
show (IntResult i) = "IntResult " ++ show i
show (BoolResult b) = "BoolResult " ++ show b
show (StringResult s) = "StringResult \"" ++ s ++ "\""
----------------------------------------------
-- Define Interpreter
interpPrim : Prim e -> Maybe Result
interpPrim (MkPrim "read" HNil) = Just (IntResult 42) -- Replace with actual read operation
interpPrim (MkPrim "-" (MkIntExp x ::: HNil)) = Just (IntResult (-x))
interpPrim (MkPrim "+" (MkIntExp x ::: MkIntExp y ::: HNil)) = Just (IntResult (x + y))
interpPrim (MkPrim "-" (MkIntExp x ::: MkIntExp y ::: HNil)) = Just (IntResult (x - y))
interpPrim _ = Nothing
main : IO ()
main = do
let x = interpPrim neg_eight
putStrLn (show x)
And here's the Lean4 code:
------------------------------------------------------------
-- Define Expression Types
structure IntExp where
x : Int
deriving Repr
structure BoolExp where
x : Bool
deriving Repr
structure StringExp where
x : String
deriving Repr
-----------------------------------------------------------
-- Define `HList`
inductive HList : List Type → Type 1 where -- fails to compile without the '1' in 'Type 1'
| nil : HList []
| cons : {t : Type} → t → HList ts → HList (t :: ts)
infixr:5 " ::: " => HList.cons
-----------------------------------------------------------
-- Define Primitive Operations
structure Prim (e : List Type) where
operation : String
arguments : HList e
def eight : IntExp := IntExp.mk 8
def neg_eight : Prim [IntExp] := Prim.mk "-" (eight ::: HList.nil)
def rd : Prim [] := Prim.mk "read" HList.nil
----------------------------------------------
-- Define Result Type
inductive Result where
| int_result : Int -> Result
| BoolResult : Bool -> Result
| StringResult : String -> Result
deriving Repr
----------------------------------------------
-- Define Interpreter
-- Doesn't compile:
-- Line that has the pattern match ({ x := 9001 } : Int Exp) :: HList.nil
-- fails to compile with the following error message:
-- pattern
-- { x := _fvar.5221 }
-- has type
-- IntExp : Type
-- but is expected to have type
-- t✝ : Type
def interpret_prim : {e: List Type} → Prim e → Option Result
| _, { operation := "read", arguments := HList.nil } => some $ Result.int_result 42 -- replace with actual read operation
| _, { operation := "-", arguments := ((IntExp.mk a) ::: HList.nil) } => some $ Result.int_result a
| _, { operation := _, arguments := _ } => none
#eval interpret_prim neg_eight
Is there no way to get this to compile?
Mario Carneiro (Aug 15 2024 at 04:53):
you can define HList to avoid a universe bump but you need to define it by recursion instead of induction
Mario Carneiro (Aug 15 2024 at 05:00):
here's a version which uses a recursive definition of HList:
-----------------------------------------------------
-- Define Expression Types
structure IntExp where
x : Int
deriving Repr
structure BoolExp where
x : Bool
deriving Repr
structure StringExp where
x : String
deriving Repr
-----------------------------------------------------------
-- Define `HList`
def HList : List Type → Type
| [] => Unit
| t :: ts => t × HList ts
-----------------------------------------------------------
-- Define Primitive Operations
structure Prim (e : List Type) where
operation : String
arguments : HList e
def eight : IntExp := IntExp.mk 8
def neg_eight : Prim [IntExp] := Prim.mk "-" (eight, ())
def rd : Prim [] := Prim.mk "read" ()
----------------------------------------------
-- Define Result Type
inductive Result where
| int_result : Int -> Result
| BoolResult : Bool -> Result
| StringResult : String -> Result
deriving Repr
----------------------------------------------
-- Define Interpreter
def interpret_prim : {e: List Type} → Prim e → Option Result
| [], { operation := "read", arguments := () } => some $ Result.int_result 42 -- replace with actual read operation
| .([IntExp]), { operation := "-", arguments := (IntExp.mk a, ()) } => some $ Result.int_result a
| _, { operation := _, arguments := _ } => none
#eval interpret_prim neg_eight
Mario Carneiro (Aug 15 2024 at 05:03):
However, it doesn't fix the main issue, and I think you can't really do this in lean. The trouble is that in your second pattern match, the expression ((IntExp.mk a) ::: HList.nil)
forces a certain type for e: List Type
, which is to say the _
is actually [IntExp]
here, but while you can pattern match the List Type
to have form [_]
, you can't use pattern matching to make it IntExp
without deciding type equality.
Mario Carneiro (Aug 15 2024 at 05:04):
The right solution to this is the universe pattern
Mario Carneiro (Aug 15 2024 at 05:08):
-----------------------------------------------------
-- Define Expression Types
structure IntExp where
x : Int
deriving Repr
structure BoolExp where
x : Bool
deriving Repr
structure StringExp where
x : String
deriving Repr
inductive TypeCode where
| int
| bool
| string
def TypeCode.toExp : TypeCode → Type
| .int => IntExp
| .bool => BoolExp
| .string => StringExp
-----------------------------------------------------------
-- Define `HList`
def HList : List TypeCode → Type
| [] => Unit
| t :: ts => t.toExp × HList ts
-----------------------------------------------------------
-- Define Primitive Operations
structure Prim (e : List TypeCode) where
operation : String
arguments : HList e
def eight : IntExp := IntExp.mk 8
def neg_eight : Prim [.int] := Prim.mk "-" (eight, ())
def rd : Prim [] := Prim.mk "read" ()
----------------------------------------------
-- Define Result Type
inductive Result where
| int_result : Int -> Result
| BoolResult : Bool -> Result
| StringResult : String -> Result
deriving Repr
----------------------------------------------
-- Define Interpreter
def interpret_prim : {e: List TypeCode} → Prim e → Option Result
| [], { operation := "read", arguments := () } => some $ Result.int_result 42 -- replace with actual read operation
| [.int], { operation := "-", arguments := (IntExp.mk a, ()) } => some $ Result.int_result a
| _, { operation := _, arguments := _ } => none
#eval interpret_prim neg_eight
James Good (Aug 15 2024 at 22:19):
Mario Carneiro said:
The right solution to this is the universe pattern
Thank you for taking the time to reply.
I can understand why this shouldn't work after seeing the explanation, regarding the identity function, as to why you wouldn't want to pattern match on types.
However, I suppose my initial reason for expecting it to work is because the Idris2 version does compile, whereas the Lean4 version does not.
I'll have to give chapter 8 of the Functional Programming in Lean book a read.
Yuri (Aug 27 2024 at 19:16):
Out of curiosity, what is the underlying reason why Idris compiles the original code fine and Lean doesn't?
Mario Carneiro (Aug 27 2024 at 22:01):
maybe @David Thrane Christiansen knows?
David Thrane Christiansen (Aug 28 2024 at 04:35):
I'm a bit out of the loop on modern Idris, but my understanding is that pattern matching the universe is allowed. Type erasure and something akin to parametricity (don't know if it is exactly that or not) is then achieved through the quantity system, by declaring them irrelevant. This is just a vague recollection, though - a quick search didn't point me to anywhere in the docs that describes this in particular.
Last updated: May 02 2025 at 03:31 UTC