Zulip Chat Archive
Stream: Is there code for X?
Topic: packing and unpacking arguments
Jianlin Li (Aug 15 2025 at 04:18):
Hi everyone! :wave:
Does Lean have anything like Python’s **kwargs (keyword-argument unpacking)?
Lean lets me call functions with named arguments out of order, e.g.:
def f (n : Nat) (m : Nat) : Nat := n * m
#eval f 3 4 -- 12
#eval f (n := 5) (m := 6) -- 30
#eval f (m := 6) (n := 5) -- 30
#check f (m := 10) -- fun n => f n 10 : Nat → Nat
What I’m looking for is the Python-style behavior where a runtime dictionary can be unpacked into named arguments:
def f(n=0, m=0): return n*m
kwargs = {"n": 3, "m": 4}
f(**kwargs) # 12
Is there a way in Lean to pass a runtime map (e.g., HashMap String Nat) and have it supply named parameters to a function? If not built-in, are there libraries, patterns, or metaprogramming approaches to simulate this (possibly via records/structures, HashMaps, currying/uncurrying, or macros)?
Michael MacLeod (Aug 15 2025 at 05:39):
Here's one option. It's a bit of a hack, but I think it does what you want. It uses default parameters and looks them up from an args hashmap if they are zero:
def f (n : Nat := 0) (m : Nat := 0) (args : Std.HashMap String Nat := .emptyWithCapacity 0) :=
let n := args.get? "n" |>.getD n
let m := args.get? "m" |>.getD m
n*m
#eval f -- 0
#eval f 2 3 -- 6
#eval f (args := .ofList [("n", 3), ("m", 4)]) -- 12
I'm not too sure what the use of doing this is though. Maybe default parameters are all you need?
Kim Morrison (Aug 15 2025 at 06:12):
I think @Jianlin Li is asking about a macro that would make this possible for any function, not how to implement a single function this way.
Siddhartha Gadgil (Aug 17 2025 at 14:31):
Should be possible with an elaborator matching the type of f as a pi-type. Will give it a shot if nobody else does it first.
An issue: what is the type of the HashMap? We need something polymorphic unless the arguments all have the same type. Maybe some form of HList should be used.
Siddhartha Gadgil (Aug 17 2025 at 14:37):
And a search for HList brings up Mario's suggestion to use Prod as HList which fits well here.
Siddhartha Gadgil (Aug 17 2025 at 15:48):
Here is the start of an implementation.
import Lean
open Lean Meta Elab Term
def isProd? (e: Expr) : MetaM (Option <| Expr × Expr) := do
let u ← mkFreshLevelMVar
let v ← mkFreshLevelMVar
let u := mkSort <| Level.succ u
let v := mkSort <| Level.succ v
let α ← mkFreshExprMVar u
let β ← mkFreshExprMVar v
let x ← mkFreshExprMVar α
let y ← mkFreshExprMVar β
let p ← mkAppM ``Prod.mk #[x, y]
if ← isDefEq p e then
return some (x, y)
return none
partial def readKeyVal (e: Expr) : MetaM (Option <| Name × Expr) := do
match ← isProd? e with
| some (kExpr, v) => do
let α ← inferType kExpr
unless ← isDefEq α (mkConst ``Name) do
return none
let key ← unsafe evalExpr Name (mkConst ``Name) kExpr
return (key, v)
| none => do
return none
partial def readKVs (e: Expr) : MetaM (Std.HashMap Name Expr) := do
match ← readKeyVal e with
| some (key, value) => do
let mut result := Std.HashMap.empty
result := result.insert key value
return result
| none => do
match ← isProd? e with
| some (head, tail) => do
match ← readKeyVal head with
| some (key, value) => do
let mut result ← readKVs tail
result := result.insert key value
return result
| none => do
return Std.HashMap.empty
| none => do
return Std.HashMap.empty
-- from batteries
def getExplicitArgsFromType : Expr → Array Name → Array Name
| .forallE n _ body bi, args =>
getExplicitArgsFromType body <| if bi.isExplicit then args.push n else args
| _, args => args
def fillFuncArgs (f: Expr) (kwArgs : Std.HashMap Name Expr) : TermElabM Expr := do
let mut args : Array Expr := #[]
let explNames := getExplicitArgsFromType (← inferType f) #[]
for n in explNames do
match kwArgs.get? n with
| some v => args := args.push v
| none => return f
Term.synthesizeSyntheticMVarsNoPostponing
mkAppM' f args
elab f:term "(**" kw:term ")" : term => do
let f ←
withoutPostponing do elabTerm f none
let kw ←
withoutPostponing do elabTerm kw none
let kwArgs ← readKVs kw
fillFuncArgs f kwArgs
def kwargs := ((`a, 1), (`b, 2), (`x, "hello"))
def f (a b :Nat) := a + b
#eval f (** ((`a, 1), (`b, 2), (`x, "hello"))) -- 3
#eval f (** kwargs) -- 3
Siddhartha Gadgil (Aug 17 2025 at 15:53):
This is not tested much. One may also want nicer syntax, but a macro should fix that.
Siddhartha Gadgil (Aug 18 2025 at 04:12):
Here is the code with some syntax added:
import Lean
open Lean Meta Elab Command Syntax Term Parser
def isProd? (e: Expr) : MetaM (Option <| Expr × Expr) := do
let u ← mkFreshLevelMVar
let v ← mkFreshLevelMVar
let u := mkSort <| Level.succ u
let v := mkSort <| Level.succ v
let α ← mkFreshExprMVar u
let β ← mkFreshExprMVar v
let x ← mkFreshExprMVar α
let y ← mkFreshExprMVar β
let p ← mkAppM ``Prod.mk #[x, y]
if ← isDefEq p e then
return some (x, y)
return none
partial def kv? (e: Expr) : MetaM (Option <| Name × Expr) := do
match ← isProd? e with
| some (kExpr, v) => do
let α ← inferType kExpr
unless ← isDefEq α (mkConst ``Name) do
return none
let key ← unsafe evalExpr Name (mkConst ``Name) kExpr
return (key, v)
| none => do
return none
partial def kvs? (e: Expr) : MetaM (Std.HashMap Name Expr) := do
match ← kv? e with
| some (key, value) => do
let mut result := Std.HashMap.empty
result := result.insert key value
return result
| none => do
match ← isProd? e with
| some (head, tail) => do
match ← kv? head with
| some (key, value) => do
let mut result ← kvs? tail
result := result.insert key value
return result
| none => do
return Std.HashMap.empty
| none => do
return Std.HashMap.empty
-- from batteries
def getExplicitArgsFromType : Expr → Array Name → Array Name
| .forallE n _ body bi, args =>
getExplicitArgsFromType body <| if bi.isExplicit then args.push n else args
| _, args => args
def fillFuncKWArgs (f: Expr) (kwArgs : Std.HashMap Name Expr) : TermElabM Expr := do
let mut args : Array Expr := #[]
let explNames := getExplicitArgsFromType (← inferType f) #[]
for n in explNames do
match kwArgs.get? n with
| some v => args := args.push v
| none => return f
Term.synthesizeSyntheticMVarsNoPostponing
mkAppM' f args
elab f:term "(**" kw:term ")" : term => do
let f ←
withoutPostponing do elabTerm f none
let kw ←
withoutPostponing do elabTerm kw none
let kwArgs ← kvs? kw
fillFuncKWArgs f kwArgs
def kwargsEg := ((`a, 1), (`b, 2), (`x, "hello"))
def f (a b :Nat) := a + b
#eval f (** ((`a, 1), (`b, 2), (`x, "hello"))) -- 3
#eval f (** kwargsEg) -- 3
syntax assgn := ident " := " term
syntax (name:=kwargs) "{{" assgn,* "}}" : term
def kwTerm : TSyntax ``assgn → MacroM Syntax.Term
| `(assgn| $n:ident := $v:term) => do
let n := quoteNameMk n.getId
`(($n, $v))
| _ => throw Lean.Macro.Exception.unsupportedSyntax
macro_rules
| `({{ $p:assgn }}) =>
kwTerm p
| `({{ $as:assgn,*, $last:assgn }}) => do
let head ← kwTerm last
let tailPairs ← Array.mapM kwTerm as
let stx ← tailPairs.foldrM (init := head) (fun acc p => `(($acc, $p)))
return stx
#eval f (** kwargsEg) -- 3
def kwArgsEg' := {{ a := 1, b := 2, c := "hello" }}
#eval kwargsEg
#eval kwArgsEg'
#eval f (** kwArgsEg')
#eval f (** {{ a := 1, b := 2, c := "hello" }}) -- 3
Siddhartha Gadgil (Aug 18 2025 at 04:15):
The doubled braces were to avoid collisions with structures but Lean seems smart enough that they are not needed.
Adam Topaz (Aug 25 2025 at 16:55):
Siddhartha Gadgil said:
Should be possible with an elaborator matching the type offas a pi-type. Will give it a shot if nobody else does it first.An issue: what is the type of the
HashMap? We need something polymorphic unless the arguments all have the same type. Maybe some form of HList should be used.
We do have docs#Std.DHashMap
Last updated: Dec 20 2025 at 21:32 UTC