CurryN (version 1)

该比赛已结束,您无法在比赛模式下递交该题目。您可以点击“在题库中打开”以普通模式查看和递交本题。

本题暂时只支持 lean4。

有个东西叫作柯里化。

比如在大家比较熟悉的 C++ python 中,大部分函数都是"非柯里化"(uncurried)的—— 调用时一次把全部参数给齐:

int add(int a, int b) { return a + b; }
add(1, 2); // 一次给两个参数,返回 3
def add(a, b):
    return a + b

add(1, 2)  # 一次给两个参数,返回 3

这样写没什么问题。但有时候你只想先固定一部分参数,把剩下的参数"留到以后再给" (partial application / 部分求值)。比如想做一个"加一函数",非柯里化语言里 就得绕一下:

#include <functional>
auto add1 = std::bind(add, 1, std::placeholders::_1);
add1(2); // 3
from functools import partial
add1 = partial(add, 1)
add1(2)  # 3

而"柯里化"(currying)就是把 add 改写成"一次只吃一个参数、返回一个新函数" 的形式:

auto add_c = [](int a) { return [a](int b) { return a + b; }; };
add_c(1)(2); // 3
def add_c(a):
    return lambda b: a + b

add_c(1)(2)  # 3

柯里化之后,"部分求值"就是免费的:add_c(1) 本身就是一个合法的"加一函数", 不需要 bind / partial 之类的额外包装。

在很多编程语言中,柯里化和反柯里化是可以对任何二元函数进行的(也就是高阶函数),比如在 haskell 和 lean 中:

curry :: ((a, b) -> c) -> a -> b -> c
curry f x y = f (x, y)

uncurry :: (a -> b -> c) -> (a, b) -> c
uncurry f (x, y) = f x y
def curry {α β γ : Type} (f : α × β → γ) (x : α) (y : β) : γ := f (x, y)

def uncurry {α β γ : Type} (f : α → β → γ) (p : α × β) : γ := f p.1 p.2

看,curry 把一个"吃二元组的函数"变成"吃一个参数、再吃一个参数"的柯里化 函数,uncurry 则是反方向。

那么问题来了,以上代码是针对二元函数的,三元函数呢?

把二元组换成三元组、把套一层换成套两层,手写一个 curry3 并不难:

curry3 :: ((a, b, c) -> d) -> a -> b -> c -> d
curry3 f x y z = f (x, y, z)

uncurry3 :: (a -> b -> c -> d) -> (a, b, c) -> d
uncurry3 f (x, y, z) = f x y z
def curry3 {α β γ δ : Type} (f : α × β × γ → δ) (x : α) (y : β) (z : γ) : δ := f (x, y, z)

def uncurry3 {α β γ δ : Type} (f : α → β → γ → δ) (p : α × β × γ) : δ := f p.1 p.2.1 p.2.2

四元函数呢?再套一层。五元?再套一层……

N元函数呢?

我们发现好像在类型层面并不容易写出这个东西的 “类型”

  • 疑问一:N 元的 curry 函数是由很多个箭头拼出来的,如何“定位”到最后一个箭头?
  • 疑问二:对于 uncurried 的函数,这个“参数列表”是什么? 是 N 元的 tuple 吗?还是别的?

因为疑问二的存在,不要求使用唯一的“数据结构”来装这个“参数列表”,我们会使用一些更灵活的方式测试。

这时候有人要说了,我可以使用 templeteHaskell !

没错,这个 curryN 常常被用作 templeteHaskell 的某种例题。

但是呢,我们希望你使用纯类型的方法,来“寻觅” curryN 的定义。


那么怎么评测呢?

来讲一个小小的故事。

从前有一天,出题人在写 haskell,他的程序出现了(类似)这样的几个函数:

如果你不会 haskell 只会lean 可以让 ai 把下面这段的代码翻译成 lean4.

concat3 a b c = a ++ b ++ c 
add4 a b c d = a + b + c + d
append arr a = arr ++ [a]


他当时想要这么一个函数, f,满足一下条件:

-- show 是把任何一个可表示的类型转化成一个 string 的函数\

f concat3 == \a b c -> "<" ++ (show (a ++ b ++ c)) ++">"
f add4 == \a b c d -> "<" ++ (show (a + b + c + d)) ++">"
f append == \arr a -> "<" ++ (show (arr ++ [a])) ++">"

(==) 这里指 equivalent (数学上等价)

他想,如果把这个函数实现出来该多好!

可惜,他不会构造出这个函数。


但是现在我们有 curryN 和 uncurryN 了!

不妨

ttt :: Show a => a -> String
ttt s = "<" ++ show s ++ ">"

那

curry $ ttt . uncurry concat3 就是 f concat3 啊!

curry $ ttt . uncurry add4 就是 f add4 啊!

curry $ ttt . uncurry append 就是 f append 啊!

你看这不就接近构造出 f 了吗?


基于这个故事,出题人打算这么测试:

你要在 lean4 里面实现 curry 和 uncurry 两个函数(名字固定为 curry / uncurry, 禁止用元编程),然后让形如下面的程序得到正确的结果:

def concat3 (a : String) (b : String) (c : String) := a ++ b ++ c

def add4 (a : Int) (b : Int) (c : Int) (d : Int) := a + b + c + d

def append (arr : List Int) (a : Int) := arr ++ [a]

def ttt [ToString α] (s : α) := "<" ++ ToString.toString s ++ ">"

def mycomp'a := curry $ ttt ∘ uncurry concat3
def mycomp'b := curry $ ttt ∘ uncurry add4
def mycomp'c := curry $ ttt ∘ uncurry append

#eval mycomp'a "Hello, " "World" "!"
#eval mycomp'b 1 2 3 4
#eval mycomp'c [1, 2, 3] 4

也就是说,因为 curryN 和 uncurryN 的非柯里函数的参数表的类型是你们自己定义的,

我打算只测一条行为规范:

∀ f, trans: curry (trans ∘ uncurry f) x₁ … xₙ = trans (f x₁ … xₙ)


我们需要你支持依赖值类型,但不要求支持隐式参数和类型类参数的推导,因为 curryN 可能会阻断 lean4 的某些类型推倒。

出题人会可能在一两天之后添加无须支持依赖值类型的 curryN 部分分

评测

  • 你只需要提交 CurryN.lean。
  • 公开测试:随题目给出(PublicTests.lean),包含上面这类调用以及依赖 类型的例子(如 (n : Nat) → Fin n → Nat)。你需要把它和 你的 CurryN.lean 拼接后编译。
  • 隐藏测试:评测时组织者会加入 Grader.lean,包含一些隐藏测试。
  • 限制:禁止元编程(elab / macro / syntax)、unsafe、axiom、 sorry、partial、import;只能使用 Init 自带内容。

PublicTests.lean如下:

section test'sample

def concatMix4 (a:Nat) (b:String) (c:String) (d:Nat) :=
  toString a ++ b ++ c ++ toString d


def ttt s := "<" ++ s ++ ">"

def mycomp4 := curry $ ttt ∘ uncurry concatMix4

#eval mycomp4 1 "a" "b" 2

def concatMix5 (a:Nat) (b:String) (c:String) (d:Nat) (e : Bool):=
  if e then
    toString a ++ b ++ c ++ toString d
  else
    toString d ++ c ++ b ++ toString a

def mycomp5 := curry $ ttt ∘ uncurry concatMix5
#eval mycomp5 1 "a" "b" 2 true
#eval mycomp5 1 "a" "b" 4 false

-- 类型类多态变换 + Int / List 参数
def concat3 (a : String) (b : String) (c : String) := a ++ b ++ c

def add4 (a : Int) (b : Int) (c : Int) (d : Int) := a + b + c + d

def append (arr : List Int) (a : Int) := arr ++ [a]

def ttt2 [ToString α] (s : α) := "<" ++ ToString.toString s ++ ">"

def mycomp'a := curry $ ttt2 ∘ uncurry concat3
def mycomp'b := curry $ ttt2 ∘ uncurry add4
def mycomp'c := curry $ ttt2 ∘ uncurry append

#eval mycomp'a "Hello, " "World" "!"
#eval mycomp'b 1 2 3 4
#eval mycomp'c [1, 2, 3] 4

end test'sample

clarifications

  1. 本题评测即编译,通过编译即代表评测成功,编译时会把你提交的代码和评测的代码拼在一起编译

Hello GeekPie_ 2026

未参加
状态
已结束
规则
IOI
题目
23
开始于
2026-9-19 0:00
结束于
2026-10-3 0:00
持续时间
336 小时
主持人
参赛人数
227