Zulip Chat Archive

Stream: new members

Topic: nice way to implement instances on mutually defined types?


JJ (Aug 16 2025 at 05:29):

I have the following code that implements ToString for a simple Expr type:

def Index := Nat
deriving ToString

inductive Expr where
| abs (body : Expr)
| app (func arg : Expr)
| var (idx : Index)

instance : ToString Expr := toString
where toString
| .abs body => s!"(λ {toString body})"
| .app func arg => s!"{toString func} {toString arg}"
| .var idx => s!"{idx}"

I'm pretty happy with the aesthetics of this code. The where declaration lets the toString helper avoid polluting the namespace. On the other hand, with mutually-defined inductive datatypes, I can't seem to avoid ugly naming conventions:

mutual
inductive Value where
| clos (body : Expr) (env : Array Value)
| ntrl (neutral : Neutral)

inductive Neutral where
| nvar (idx : Index)
| napp (func : Neutral) (arg : Value)
end

mutual
def toString_Value : Value -> String
| .clos body _ => s!"(λ {body})"
| .ntrl neutral => toString_Neutral neutral
def toString_Neutral : Neutral -> String
| .nvar idx => s!"{idx}"
| .napp func arg => s!"{toString_Neutral func} {toString_Value arg}"
end

instance : ToString Value := toString_Value
instance : ToString Neutral := toString_Neutral

Does anyone have suggestions for how to clean this code up?

Aaron Liu (Aug 16 2025 at 10:05):

I would call those Neutral.toString and Value.toString

JJ (Aug 16 2025 at 15:13):

Oh, it slipped my mind I could just name them that. Thanks. No way to get around the mutual block, right?

Kyle Miller (Aug 16 2025 at 16:14):

Using a mutual block is the standard way to write these.

By the way, it's usually better having a separate def, rather than pack everything into the instance. Last I checked, the compiler will eagerly unfold instances, which causes much more inlining than you'd expect. That means slower compilation and larger binaries, if that matters to you.


Last updated: Dec 20 2025 at 21:32 UTC