15장: 모나드, do 표기법, IO
새 파일
src/io_lower.rs변경된 파일
src/desugar.rssrc/eval.rssrc/typecheck.rssrc/ast.rssrc/data.rssrc/elaborate.rssrc/fixity.rssrc/kinds.rssrc/layout.rssrc/lexer.rssrc/lib.rssrc/main.rssrc/primitives.rssrc/rename.rssrc/session.rs
동기
지금까지 우리 프로그램은 바깥 세계와 꽤 제한된 관계만 맺어 왔습니다. 드라이버는 main을 평가하고, 답을 출력한 뒤, 퇴근합니다. 질문을 던지고, 응답을 읽고, 다음에 무엇을 할지 결정할 수 있는 프로그램을 작성해 봅시다.
우리는 일반 표현식을 순수하게 유지하고 싶으므로, IO 값을 평가해도 그 효과는 수행되지 않습니다. 우리는 동작(action)을 값으로 표현하고, 명시적인 경계에서 그것을 실행할 것입니다. 이 구분 덕분에 우리가 이미 알고 있는 함수들을 사용해 동작을 만들고 조합할 수 있습니다.
14장의 타입 클래스는 이러한 조합을 기술하는 방법을 제공합니다. Functor, Applicative, Monad는 IO와 함께 동작하지만, Maybe, Either, 리스트에 대한 유용한 연산도 기술합니다. 우리는 먼저 이 연산들이 무엇을 하는지 살펴보고, 구문을 손으로 직접 전개해 보겠습니다. 용어에 신비로움을 덧씌우지 않아도 여기에는 이해할 것이 충분히 많습니다.
현재의 언어
이제 완전한 프로그램은 그 시그니처를 직접 작성했든 추론되었든 main :: IO ()를 가집니다. 가장 작은 예제는 새 Prelude 프린터를 호출합니다:
main = putStrLn "hello"
do 표기법은 일반 데이터에도 쓸 수 있습니다. 여기서는 두 번의 바인드가 Just에서 값을 꺼내고, pure가 같은 Maybe 인스턴스 안에서 결과를 만듭니다:
main = print (do { x <- Just 20; y <- Just 22; pure (x+y) })
블록은 반드시 식으로 끝나야 합니다. x <- action 문은 남은 문에서만 x를 바인딩합니다. let 문은 in 없이 지역 선언을 도입합니다. 패턴은 중첩될 수 있으며, 여기에는 튜플, 리스트, as-패턴, 명목적 레코드 패턴이 포함됩니다. 반박 가능한 패턴이 매칭에 실패하면 그 소스 위치와 함께 error를 호출합니다. Fun에는 MonadFail이 없으므로, 둘러싼 모나드가 Maybe일 때에도 실패는 런타임 오류가 됩니다.
putStr, putStrLn, getLine, print, exitWith는 프로그램의 외부 인터페이스를 제공합니다. mapM, mapM_, sequence_, when, unless는 흔히 쓰이는 순서화 패턴을 표현합니다. 진입점이 없거나 타입이 잘못되었으면 E0250을 보고합니다. 본문이 명시적 시그니처를 위반하면 그 대신 시그니처 검사 중에 E0203으로 실패할 수 있습니다.
이론
어떤 액션이 a를 만들어 내고, 프로그램의 나머지 부분은 그 a를 써서 다음 액션을 만드는 법을 안다고 가정해 봅시다. 바인드는 이 둘을 연결합니다. 두 번째 인자가 함수인 까닭은 다음 액션이 첫 번째 결과에 따라 달라질 수 있기 때문입니다. 이 타입을 왼쪽에서 오른쪽으로 천천히 읽어 보십시오 (Jones 1993).
IO의 경우, 소스 검사기는 IO를 추상적인 명목 생성자로 취급합니다. Core는 액션을 unit에서 그 결과로 가는 함수로 표현합니다:
unit 인자는 지연을 담아 둘 자리를 마련해 줍니다. 함수를 만든다고 해서 그 본문이 실행되지는 않습니다. returnIO는 저장해 둔 값을 반환하는 함수를 만들고, bindIO는 첫 번째 액션을 실행한 뒤 그 결과를 다음 액션에 넘기는 함수를 만듭니다. 부수 효과를 일으키는 기본 연산들은 그 본문 안에 들어 있습니다.
이 표현은 신뢰할 수 있는 Prelude와 컴파일러만 사용할 수 있습니다. 소스 프로그램에는 추상적인 IO a 값만 보이므로, 액션에 ()를 적용해 아무 곳에서나 실행할 수 없습니다. 바로 이 경계 덕분에 일반적인 Fun 식이 순수하게 유지됩니다.
엄격성은 여전히 적용됩니다. putStrLn "hello"를 만들면 그 문자열 인자를 평가하고 액션을 반환합니다. 출력은 하지 않습니다. error "bad"를 즉시 요구되는 인자로 넘겨 액션을 만들면, 액션이 실행되기 전에 실패할 수 있습니다. 순서화가 제어하는 것은 효과이지, 임의의 순수한 인자의 평가가 아닙니다. Haskell Report는 이와 유사한 do 변환을 기술하지만 (Simon Marlow 2010), 그 지연 평가를 Fun의 연산적 설명에 들여와서는 안 됩니다.
모나드 법칙은 인스턴스가 따라야 할 동작을 기술합니다. 컴파일러는 타입을 검사할 뿐, 그 등식들을 검사하지는 않습니다. 특히 비종료가 관련된 등식은 엄격한 언어에서 주의해서 다뤄야 합니다. 메커니즘은 여전히 14장의 사전 전달입니다. 평가기에 새로운 클래스 디스패치는 나타나지 않습니다.
구현
파서는 어휘적 이름 변경 전에 desugar::do_block을 호출합니다. 문장을 뒤에서부터 처리하면 남은 블록이 각 바인드의 컨티뉴에이션이 됩니다. 변수 패턴은 곧바로 람다가 됩니다. 반박 가능한 패턴은 실패 대안을 가진 case를 도입합니다. 이때 쓰는 임시 이름은 소스 식별자로 작성할 수 없습니다.
#![allow(unused)]
fn main() {
use crate::{
data::{ConId, DataEnv},
diagnostics::{Diagnostic, ErrorCode},
renamed::{self, BindGroup, Expr, Kind, Pat, PatKind, Row, RowBody},
session::{Session, Stop},
source::Span,
syn,
};
struct Desugar<'a> {
sess: &'a mut Session,
data: &'a DataEnv,
}
impl Desugar<'_> {
fn node(&mut self, kind: Kind, span: Span) -> Expr {
Expr {
kind,
span,
node: self.sess.fresh_node(),
}
}
fn constructor(&mut self, name: &str, span: Span) -> ConId {
if let Some(info) = self.data.constructor(name) {
return info.id.clone();
}
self.sess.emit(Diagnostic::error(
ErrorCode::UnboundConstructor,
format!("sugar requires constructor {name} from the Prelude"),
span,
));
ConId {
index: 0,
name: name.into(),
}
}
fn tuple(&mut self, n: usize, span: Span) -> ConId {
if !matches!(n, 0 | 2 | 3) {
self.sess.emit(Diagnostic::error(
ErrorCode::Parse,
"only unit, pairs, and triples are supported",
span,
));
}
self.constructor(
&format!("({})", ",".repeat(n.saturating_sub(1))),
span,
)
}
fn pattern(&mut self, p: Pat) -> Pat {
let span = p.span;
let kind = match p.kind {
PatKind::List(ps) => {
let nil = self.constructor("[]", span);
let cons = self.constructor(":", span);
let mut tail = Pat {
kind: PatKind::Con(nil, vec![]),
span,
};
for p in ps.into_iter().rev() {
tail = Pat {
kind: PatKind::Con(
cons.clone(),
vec![self.pattern(p), tail],
),
span,
};
}
return tail;
}
PatKind::Tuple(ps) => PatKind::Con(
self.tuple(ps.len(), span),
ps.into_iter().map(|p| self.pattern(p)).collect(),
),
PatKind::Con(id, ps) => PatKind::Con(
id,
ps.into_iter().map(|p| self.pattern(p)).collect(),
),
PatKind::As(id, p) => {
PatKind::As(id, Box::new(self.pattern(*p)))
}
PatKind::Int(n) => PatKind::Int((n << 1) >> 1),
other => other,
};
Pat { kind, span }
}
fn groups(&mut self, groups: Vec<BindGroup>) -> Vec<BindGroup> {
groups
.into_iter()
.map(|mut g| {
for b in &mut g.binds {
b.rhs = self.expression(b.rhs.clone());
}
g
})
.collect()
}
fn row(&mut self, row: Row) -> Row {
let body = match row.body {
RowBody::Plain(e) => {
RowBody::Plain(Box::new(self.expression(*e)))
}
RowBody::Guarded(gs) => RowBody::Guarded(
gs.into_iter()
.map(|(g, e)| (self.expression(g), self.expression(e)))
.collect(),
),
};
Row {
patterns: row
.patterns
.into_iter()
.map(|p| self.pattern(p))
.collect(),
body,
locals: self.groups(row.locals),
span: row.span,
}
}
fn equations(&mut self, rows: Vec<Row>, span: Span) -> Expr {
let mut rows: Vec<_> =
rows.into_iter().map(|r| self.row(r)).collect();
if rows.len() == 1
&& rows[0]
.patterns
.iter()
.all(|p| matches!(p.kind, PatKind::Var(_)))
&& matches!(rows[0].body, RowBody::Plain(_))
{
let row = rows.pop().unwrap();
let RowBody::Plain(body) = row.body else {
unreachable!()
};
let mut body = *body;
if !row.locals.is_empty() {
body =
self.node(Kind::Let(row.locals, Box::new(body)), span);
}
for p in row.patterns.into_iter().rev() {
let PatKind::Var(id) = p.kind else {
unreachable!()
};
body = self.node(Kind::Lam(id, Box::new(body)), span);
}
return body;
}
// ANCHOR: equations-match
let params: Vec<_> = (0..rows[0].patterns.len())
.map(|_| self.sess.fresh_id("$match"))
.collect();
let scruts = params
.iter()
.map(|id| self.node(Kind::Var(id.clone()), span))
.collect();
let mut body = self.node(Kind::Match(scruts, rows), span);
for id in params.into_iter().rev() {
body = self.node(Kind::Lam(id, Box::new(body)), span);
}
body
// ANCHOR_END: equations-match
}
fn expression(&mut self, expr: Expr) -> Expr {
let mut work = vec![(Some(expr), None)];
let mut values: Vec<Expr> = Vec::new();
while let Some((expr, saved)) = work.pop() {
match expr {
Some(Expr {
kind: Kind::App(f, a),
node,
span,
}) => {
work.push((None, Some((node, span))));
work.push((Some(*a), None));
work.push((Some(*f), None));
}
Some(expr) => values.push(self.expression_leaf(expr)),
None => {
let a = values.pop().expect("application argument");
let f = values.pop().expect("application function");
let (node, span) = saved.expect("application metadata");
values.push(Expr {
node,
span,
kind: Kind::App(Box::new(f), Box::new(a)),
});
}
}
}
values.pop().expect("desugared root")
}
fn expression_leaf(&mut self, expr: Expr) -> Expr {
let span = expr.span;
let kind = match expr.kind {
Kind::Record(id, fields) => Kind::Record(
id,
fields
.into_iter()
.map(|(n, e)| (n, self.expression(e)))
.collect(),
),
Kind::Project(e, n) => {
Kind::Project(Box::new(self.expression(*e)), n)
}
Kind::Update(e, fields) => Kind::Update(
Box::new(self.expression(*e)),
fields
.into_iter()
.map(|(n, e)| (n, self.expression(e)))
.collect(),
),
Kind::Annotated(e, t) => {
Kind::Annotated(Box::new(self.expression(*e)), t)
}
Kind::Bool(b) => Kind::Con(
self.constructor(if b { "True" } else { "False" }, span),
),
Kind::Equations(rows) => return self.equations(rows, span),
Kind::PatternLam(patterns, body) => {
return self.equations(
vec![Row {
patterns,
body: RowBody::Plain(body),
locals: vec![],
span,
}],
span,
);
}
Kind::Case(e, rows) => Kind::Match(
vec![self.expression(*e)],
rows.into_iter().map(|r| self.row(r)).collect(),
),
Kind::If(c, a, b) => {
let c = self.expression(*c);
let a = self.expression(*a);
let b = self.expression(*b);
let rows = [("True", a), ("False", b)]
.into_iter()
.map(|(name, body)| Row {
patterns: vec![Pat {
kind: PatKind::Con(
self.constructor(name, span),
vec![],
),
span,
}],
body: RowBody::Plain(Box::new(body)),
locals: vec![],
span,
})
.collect();
Kind::Match(vec![c], rows)
}
Kind::List(es) => {
let nil = self.constructor("[]", span);
let cons = self.constructor(":", span);
let mut tail = self.node(Kind::Con(nil), span);
for e in es.into_iter().rev() {
let con = self.node(Kind::Con(cons.clone()), span);
let head = self.expression(e);
let app = self.node(
Kind::App(Box::new(con), Box::new(head)),
span,
);
tail = self.node(
Kind::App(Box::new(app), Box::new(tail)),
span,
);
}
return tail;
}
Kind::Tuple(es) => {
let id = self.tuple(es.len(), span);
let mut value = self.node(Kind::Con(id), span);
for e in es {
let e = self.expression(e);
value = self.node(
Kind::App(Box::new(value), Box::new(e)),
span,
);
}
return value;
}
Kind::Lam(id, b) => {
Kind::Lam(id, Box::new(self.expression(*b)))
}
Kind::App(a, b) => Kind::App(
Box::new(self.expression(*a)),
Box::new(self.expression(*b)),
),
Kind::Let(gs, b) => {
Kind::Let(self.groups(gs), Box::new(self.expression(*b)))
}
Kind::Neg(a) => Kind::Neg(Box::new(self.expression(*a))),
Kind::Binary(op, a, b) => Kind::Binary(
op,
Box::new(self.expression(*a)),
Box::new(self.expression(*b)),
),
Kind::Match(..) => {
unreachable!("Match is produced by this pass")
}
other => other,
};
Expr {
kind,
span,
node: expr.node,
}
}
}
/// Produce Syn after names are resolved, preserving row scopes and guard order.
pub fn desugar(
sess: &mut Session,
program: renamed::Program,
) -> Result<syn::Program, Stop> {
let groups = {
let mut pass = Desugar {
sess,
data: &program.data,
};
pass.groups(program.groups)
};
sess.finish(syn::Program {
classes: program.classes,
data: program.data,
groups,
primitives: program.primitives,
main: program.main,
span: program.span,
})
}
/// Statements are lowered before name resolution so ordinary lexical scopes apply.
pub enum Statement {
Expr(crate::ast::Expr),
Bind(crate::ast::Pat, crate::ast::Expr),
Let(Vec<crate::ast::Binding>),
}
pub fn do_pattern(
e: crate::ast::Expr,
) -> Result<crate::ast::Pat, Diagnostic> {
use crate::ast::{Kind as K, Pat as P, PatKind as PK};
let span = e.span;
let kind = match e.kind {
K::Chain(a, pairs, None) if pairs.is_empty() => {
return do_pattern(*a);
}
K::Chain(a, pairs, None)
if pairs.iter().all(|(op, _)| op == ":") =>
{
let mut elements = vec![do_pattern(*a)?];
for (_, e) in pairs {
elements.push(do_pattern(e)?);
}
let mut tail = elements.pop().unwrap();
for head in elements.into_iter().rev() {
tail = P {
kind: PK::Con(":".into(), vec![head, tail]),
span,
};
}
return Ok(tail);
}
K::PatternAs(n, p) => PK::As(n, Box::new(do_pattern(*p)?)),
K::Paren(a) => return do_pattern(*a),
K::Var(n) if n == "_" => PK::Wild,
K::Var(n) if n.starts_with(char::is_uppercase) => {
PK::Con(n, vec![])
}
K::Var(n) => PK::Var(n),
K::App(a, b) => {
let mut p = do_pattern(*a)?;
if let PK::Con(_, ps) = &mut p.kind {
ps.push(do_pattern(*b)?);
return Ok(p);
} else {
return Err(Diagnostic::error(
ErrorCode::Parse,
"invalid do pattern",
span,
));
}
}
K::Int(n) => PK::Int(n),
K::Double(n) => PK::Double(n),
K::Char(c) => PK::Char(c),
K::String(s) => PK::String(s),
K::Neg(a) => {
if let K::Int(n) = a.kind {
PK::Int(-n)
} else {
return Err(Diagnostic::error(
ErrorCode::Parse,
"invalid negative pattern",
span,
));
}
}
K::List(es) => PK::List(
es.into_iter().map(do_pattern).collect::<Result<_, _>>()?,
),
K::Tuple(es) => PK::Tuple(
es.into_iter().map(do_pattern).collect::<Result<_, _>>()?,
),
K::Update(a, fs) => {
fn record(
mut p: P,
fields: Vec<(String, P)>,
) -> Result<P, Diagnostic> {
p.kind = match p.kind {
PK::As(n, p) => {
PK::As(n, Box::new(record(*p, fields)?))
}
PK::Con(n, ps) if ps.is_empty() => {
PK::Record(n, fields)
}
_ => {
return Err(Diagnostic::error(
ErrorCode::Parse,
"invalid record pattern",
p.span,
));
}
};
Ok(p)
}
let fields = fs
.into_iter()
.map(|(n, e)| Ok((n, do_pattern(e)?)))
.collect::<Result<_, Diagnostic>>()?;
return record(do_pattern(*a)?, fields);
}
K::Record(n, fs) => PK::Record(
n,
fs.into_iter()
.map(|(n, e)| Ok((n, do_pattern(e)?)))
.collect::<Result<_, Diagnostic>>()?,
),
_ => {
return Err(Diagnostic::error(
ErrorCode::Parse,
"invalid do pattern",
span,
));
}
};
Ok(P { kind, span })
}
/// Expand do syntax into ordinary applications, matches, and local bindings.
pub fn do_block(
statements: Vec<Statement>,
span: Span,
) -> Result<crate::ast::Expr, Diagnostic> {
use crate::ast::{
Expr as E, Kind as K, Pat as P, PatKind as PK, Row as R,
RowBody as B,
};
let node = |kind| E { kind, span };
let app = |a, b| node(K::App(Box::new(a), Box::new(b)));
let mut statements = statements;
let Some(Statement::Expr(mut body)) = statements.pop() else {
return Err(Diagnostic::error(
ErrorCode::Parse,
"do must end with an expression",
span,
));
};
for statement in statements.into_iter().rev() {
body = match statement {
Statement::Expr(e) => {
app(app(node(K::Var(">>".into())), e), body)
}
Statement::Let(bs) => node(K::LetGroup(bs, Box::new(body))),
Statement::Bind(p, e) => {
let continuation = match &p.kind {
PK::Var(n) => node(K::Lam(n.clone(), Box::new(body))),
PK::Wild => node(K::Lam("$do".into(), Box::new(body))),
_ => {
let site = p.span;
let rows = vec![
R {
patterns: vec![p],
body: B::Plain(Box::new(body)),
locals: vec![],
span: site,
},
R {
patterns: vec![P {
kind: PK::Wild,
span: site,
}],
body: B::Plain(Box::new(E {
kind: K::DoFail,
span: site,
})),
locals: vec![],
span: site,
},
];
node(K::Lam(
"$do".into(),
Box::new(node(K::Case(
Box::new(node(K::Var("$do".into()))),
rows,
))),
))
}
};
app(app(node(K::Var(">>=".into())), e), continuation)
}
};
}
Ok(body)
}
}
이 배치는 이름 변경 이후에 별도로 수행하는 디슈거링 패스와 다릅니다. 생성된 바인딩도 손으로 작성한 바인딩과 같은 스코프 검사기를 거칩니다. 보조 함수 do_pattern은 <- 왼쪽에 있는 패턴 모양의 구문을 검증합니다. 스팬을 지닌 DoFail 노드는 나중에 파일, 줄, 열 정보를 담은 일반적인 오류 호출로 변환됩니다. 어떤 do 노드도 Core에 도달하지 않습니다.
검사기가 IO 구현을 unit을 받는 함수와 연관 짓게 해 주는 것은 Prelude의 신뢰된 FileId뿐입니다. 사용자에게 보이는 타입 스킴은 명목적으로 유지됩니다. 정교화(elaboration) 후 io_lower::lower는 린트 전에 모든 Core 타입을 순회합니다. 바인더 타입, 타입 적용, 생성자 필드, match 결과 타입, 사전 타입이 그 대상입니다. 가장 바깥 수준의 IO a만 대체한다면 Monad IO 사전 같은 고계 종류 사용을 놓치게 됩니다. 이 순회는 적용되지 않은 생성자 IO도 부분 적용된 함수 생성자 (->) ()로 대체합니다.
실행기는 먼저 함수 클로저를 설치하고 엄격한 최상위 값을 의존성 순서대로 평가한 다음, 진입점을 unit에 적용합니다:
#![allow(unused)]
fn main() {
use crate::{
ast::Op,
core::{self, Bind, BindGroup, Expr, Id, Lit},
};
use std::{cell::RefCell, rc::Rc};
/// Language termination is separate from an ordinary runtime failure.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Failure {
Runtime(String),
Exit(i32),
}
impl From<String> for Failure {
fn from(s: String) -> Self {
Self::Runtime(s)
}
}
impl From<&str> for Failure {
fn from(s: &str) -> Self {
Self::Runtime(s.into())
}
}
impl std::fmt::Display for Failure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Runtime(s) => write!(f, "{s}"),
Self::Exit(n) => write!(f, "exit {n}"),
}
}
}
type Env = Vec<(Id, Value)>;
#[derive(Clone, Debug)]
/// A closure captures outer declarations and an optional immutable recursive-group recipe.
pub enum Value {
Lit(Lit),
Con(crate::data::ConId, Vec<Value>, Option<Vec<String>>),
Closure {
parameter: Id,
body: Rc<Expr>,
env: Rc<Env>,
recursive: Option<Rc<Vec<Bind>>>,
},
}
impl std::fmt::Display for Value {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.print(f, 0)
}
}
impl Value {
fn print(
&self,
f: &mut std::fmt::Formatter<'_>,
precedence: u8,
) -> std::fmt::Result {
match self {
Self::Lit(Lit::Double(n))
if f64::from_bits(*n).is_sign_negative()
&& precedence > 6 =>
{
write!(
f,
"({})",
crate::primitives::show_double(f64::from_bits(*n))
)
}
Self::Lit(Lit::Int(n)) if *n < 0 && precedence > 6 => {
write!(f, "({n})")
}
Self::Lit(l) => write!(f, "{l}"),
Self::Closure { .. } => write!(f, "<function>"),
Self::Con(id, fields, Some(labels)) => {
if precedence > 10 {
write!(f, "(")?;
}
write!(f, "{} {{", id.name)?;
for (i, (name, value)) in
labels.iter().zip(fields).enumerate()
{
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{name} = ")?;
value.print(f, 0)?;
}
write!(f, "}}")?;
if precedence > 10 {
write!(f, ")")?;
}
Ok(())
}
Self::Con(id, fields, None) => match id.name.as_str() {
"[]" => write!(f, "[]"),
":" => {
write!(f, "[")?;
let mut value = self;
let mut first = true;
while let Self::Con(c, fs, _) = value {
if c.name == "[]" {
break;
}
if !first {
write!(f, ",")?;
}
first = false;
fs[0].print(f, 0)?;
value = &fs[1];
}
write!(f, "]")
}
"()" | "(,)" | "(,,)" => {
write!(f, "(")?;
for (i, v) in fields.iter().enumerate() {
if i > 0 {
write!(f, ",")?;
}
v.print(f, 0)?;
}
write!(f, ")")
}
_ => {
let parens = precedence > 10 && !fields.is_empty();
if parens {
write!(f, "(")?;
}
write!(f, "{}", id.name)?;
for v in fields {
write!(f, " ")?;
v.print(f, 11)?;
}
if parens {
write!(f, ")")?;
}
Ok(())
}
},
}
}
}
pub fn normalize(n: i64) -> i64 {
(n << 1) >> 1
}
/// Rebuild a recursive group's closures from a shared immutable recipe. The
/// recipe captures the outer environment, never the closures themselves.
// ANCHOR: recursive-closures
fn recursive_values(bindings: Rc<Vec<Bind>>, env: Rc<Env>) -> Env {
let mut values = Vec::new();
for binding in bindings.iter() {
let mut rhs = &*binding.rhs;
while let Expr::TyLam(_, inner) = rhs {
rhs = inner;
}
let Expr::Lam(parameter, body) = rhs else {
unreachable!("recursive lambda invariant")
};
let value = Value::Closure {
parameter: parameter.clone(),
body: Rc::new(*body.clone()),
env: env.clone(),
recursive: Some(bindings.clone()),
};
values.push((binding.name.clone(), value));
}
values
}
fn bind_group(
group: &BindGroup,
env: &mut Env,
fuel: &mut usize,
program: &core::Program,
globals: &RefCell<Env>,
) -> Result<(), Failure> {
if group.recursive {
env.extend(recursive_values(
Rc::new(group.binds.clone()),
Rc::new(env.clone()),
));
} else {
for binding in &group.binds {
let value =
evaluate(&binding.rhs, env, fuel, program, globals)?;
env.push((binding.name.clone(), value));
}
}
Ok(())
}
fn evaluate(
expr: &Expr,
env: &Env,
fuel: &mut usize,
program: &core::Program,
globals: &RefCell<Env>,
) -> Result<Value, Failure> {
*fuel = fuel.checked_sub(1).ok_or("step limit exceeded")?;
match expr {
Expr::Lit(l) => Ok(Value::Lit(l.clone())),
Expr::TyLam(_, body) | Expr::TyApp(body, _) => {
evaluate(body, env, fuel, program, globals)
}
Expr::Global(n) => globals
.borrow()
.iter()
.find(|(id, _)| id.unique() == n.unique())
.map(|(_, v)| v.clone())
.ok_or_else(|| {
Failure::Runtime(format!(
"internal error: uninitialized global {n}"
))
}),
Expr::Var(n) => env
.iter()
.rev()
.find(|(name, _)| name == n)
.map(|(_, v)| v.clone())
.ok_or_else(|| {
Failure::Runtime(format!("internal error: unbound {n}"))
}),
Expr::Lam(parameter, body) => Ok(Value::Closure {
parameter: parameter.clone(),
body: Rc::new(*body.clone()),
env: Rc::new(env.clone()),
recursive: None,
}),
// ANCHOR: application
Expr::App(function, argument) => {
let function = evaluate(function, env, fuel, program, globals)?;
let argument = evaluate(argument, env, fuel, program, globals)?;
if let Value::Closure {
parameter,
body,
env,
recursive,
} = &function
{
let mut local = (**env).clone();
if let Some(bindings) = recursive {
local.extend(recursive_values(
bindings.clone(),
env.clone(),
));
}
local.push((parameter.clone(), argument));
evaluate(body, &local, fuel, program, globals)
} else {
Err("internal error: non-function application".into())
}
}
// ANCHOR_END: application
Expr::Let(group, body) => {
let mut local = env.clone();
bind_group(group, &mut local, fuel, program, globals)?;
evaluate(body, &local, fuel, program, globals)
}
Expr::Con(id, _, fields) => Ok(Value::Con(
id.clone(),
fields
.iter()
.map(|e| evaluate(e, env, fuel, program, globals))
.collect::<Result<_, _>>()?,
program.constructors[id.index as usize].labels.clone(),
)),
Expr::PatternFail(span, _) => Err(failure(program, *span).into()),
Expr::Match(m) => {
let values = m
.scruts
.iter()
.map(|id| {
evaluate(
&Expr::Var(id.clone()),
env,
fuel,
program,
globals,
)
})
.collect::<Result<Vec<_>, _>>()?;
for row in &m.rows {
let mut local = env.clone();
if !row
.pats
.iter()
.zip(&values)
.all(|(p, v)| matches_pattern(p, v, &mut local))
{
continue;
}
for group in &row.locals {
bind_group(group, &mut local, fuel, program, globals)?;
}
match &row.body {
core::RowBody::Plain(e) => {
return evaluate(e, &local, fuel, program, globals);
}
core::RowBody::Guarded(gs) => {
for (g, e) in gs {
let value = evaluate(
g, &local, fuel, program, globals,
)?;
if matches!(value,Value::Con(id,_,_) if id.name=="True")
{
return evaluate(
e, &local, fuel, program, globals,
);
}
}
}
}
}
Err(failure(program, m.span).into())
}
Expr::Prim(op, _, args, _) => {
let values = args
.iter()
.map(|a| evaluate(a, env, fuel, program, globals))
.collect::<Result<Vec<_>, _>>()?;
primitive(*op, &values, program, globals)
}
}
}
fn primitive(
op: Op,
args: &[Value],
program: &core::Program,
_globals: &RefCell<Env>,
) -> Result<Value, Failure> {
if let Op::Runtime(p) = op {
return crate::primitives::evaluate(p, args, program);
}
if let [Value::Lit(Lit::String(a)), Value::Lit(Lit::String(b))] = args
&& op == Op::Append
{
return Ok(Value::Lit(Lit::String(format!("{a}{b}"))));
}
let [Value::Lit(Lit::Int(a)), Value::Lit(Lit::Int(b))] = args else {
return Err("internal error: primitive operands".into());
};
if matches!(op, Op::Eq | Op::Lt | Op::Le) {
let yes = match op {
Op::Eq => a == b,
Op::Lt => a < b,
_ => a <= b,
};
let name = if yes { "True" } else { "False" };
let c = program
.constructors
.iter()
.find(|c| c.id.name == name)
.ok_or("internal error: missing Bool constructor")?;
return Ok(Value::Con(c.id.clone(), vec![], c.labels.clone()));
}
let value = match op {
Op::Runtime(p) => {
return crate::primitives::evaluate(p, args, program);
}
Op::Add => Lit::Int(normalize(a.wrapping_add(*b))),
Op::Sub => Lit::Int(normalize(a.wrapping_sub(*b))),
Op::Mul => Lit::Int(normalize(a.wrapping_mul(*b))),
Op::Eq | Op::Lt | Op::Le => unreachable!(),
Op::Div | Op::Mod => {
if *b == 0 {
return Err("divide by zero".into());
}
let (mut q, mut r) = (a / b, a % b);
if r != 0 && (r < 0) != (*b < 0) {
q -= 1;
r += b;
}
Lit::Int(normalize(if op == Op::Div { q } else { r }))
}
Op::Append => {
return Err("internal error: string append on Int".into());
}
};
Ok(Value::Lit(value))
}
/// Evaluate top-level values exactly once in dependency order, then select main's value.
fn execute(
program: &core::Program,
mut fuel: usize,
io: bool,
) -> Result<String, Failure> {
let globals = RefCell::new(vec![]);
for group in &program.groups {
for b in &group.binds {
if core::is_function(&b.rhs) {
let value = evaluate(
&b.rhs,
&vec![],
&mut fuel,
program,
&globals,
)?;
globals.borrow_mut().push((b.name.clone(), value));
}
}
}
for group in &program.groups {
for b in &group.binds {
if !core::is_function(&b.rhs) {
let value = evaluate(
&b.rhs,
&vec![],
&mut fuel,
program,
&globals,
)?;
globals.borrow_mut().push((b.name.clone(), value));
}
}
}
let value = globals
.borrow()
.iter()
.find(|(n, _)| n.unique() == program.main.unique())
.map(|(_, v)| v.clone())
.ok_or("internal error: missing main")?;
if io {
let unit = program
.constructors
.iter()
.find(|c| c.id.name == "()")
.unwrap();
let action = Expr::App(
Box::new(Expr::Global(program.main.clone())),
Box::new(Expr::Con(unit.id.clone(), vec![], vec![])),
);
evaluate(&action, &vec![], &mut fuel, program, &globals)?;
Ok(String::new())
} else {
Ok(value.to_string())
}
}
/// Evaluate a pure fragment; retained for compiler tests and REPL inspection.
pub fn run(program: &core::Program, fuel: usize) -> Result<String, String> {
execute(program, fuel, false).map_err(|e| e.to_string())
}
fn failure(program: &core::Program, span: crate::source::Span) -> String {
let site = program
.locations
.iter()
.find(|(s, _)| *s == span)
.map(|(_, s)| s.as_str())
.unwrap_or("<unknown>:1:1");
format!("pattern match failure at {site}")
}
fn matches_pattern(p: &core::Pat, v: &Value, env: &mut Env) -> bool {
match p {
core::Pat::Wild => true,
core::Pat::Var(id) => {
env.push((id.clone(), v.clone()));
true
}
core::Pat::As(id, p) => {
env.push((id.clone(), v.clone()));
matches_pattern(p, v, env)
}
core::Pat::Lit(Lit::Double(n)) => {
matches!(v,Value::Lit(Lit::Double(m)) if f64::from_bits(*n)==f64::from_bits(*m))
}
core::Pat::Lit(l) => matches!(v,Value::Lit(actual) if l==actual),
core::Pat::Con(id, ps) => match v {
Value::Con(actual, vs, _)
if id == actual && ps.len() == vs.len() =>
{
ps.iter().zip(vs).all(|(p, v)| matches_pattern(p, v, env))
}
_ => false,
},
}
}
/// Initialize globals, then run the checked IO action exactly once.
pub fn run_io(program: &core::Program, fuel: usize) -> Result<(), Failure> {
execute(program, fuel, true).map(|_| ())
}
}
사용되지 않는 최상위 액션도 여전히 시작 시점에 초기화되는 값입니다. 다음 회귀 테스트가 보여 주듯이, 그 본문에는 진입하지 않습니다:
unused = putStrLn "bad"
main = putStrLn "ok"
평가기는 언어 오류와 요청된 프로세스 종료를 구별합니다. 이 구별 덕분에 exitWith 7이 오류 메시지로 출력되지 않습니다:
#![allow(unused)]
fn main() {
use crate::{
ast::Op,
core::{self, Bind, BindGroup, Expr, Id, Lit},
};
use std::{cell::RefCell, rc::Rc};
impl From<String> for Failure {
fn from(s: String) -> Self {
Self::Runtime(s)
}
}
impl From<&str> for Failure {
fn from(s: &str) -> Self {
Self::Runtime(s.into())
}
}
impl std::fmt::Display for Failure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Runtime(s) => write!(f, "{s}"),
Self::Exit(n) => write!(f, "exit {n}"),
}
}
}
type Env = Vec<(Id, Value)>;
#[derive(Clone, Debug)]
/// A closure captures outer declarations and an optional immutable recursive-group recipe.
pub enum Value {
Lit(Lit),
Con(crate::data::ConId, Vec<Value>, Option<Vec<String>>),
Closure {
parameter: Id,
body: Rc<Expr>,
env: Rc<Env>,
recursive: Option<Rc<Vec<Bind>>>,
},
}
impl std::fmt::Display for Value {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.print(f, 0)
}
}
impl Value {
fn print(
&self,
f: &mut std::fmt::Formatter<'_>,
precedence: u8,
) -> std::fmt::Result {
match self {
Self::Lit(Lit::Double(n))
if f64::from_bits(*n).is_sign_negative()
&& precedence > 6 =>
{
write!(
f,
"({})",
crate::primitives::show_double(f64::from_bits(*n))
)
}
Self::Lit(Lit::Int(n)) if *n < 0 && precedence > 6 => {
write!(f, "({n})")
}
Self::Lit(l) => write!(f, "{l}"),
Self::Closure { .. } => write!(f, "<function>"),
Self::Con(id, fields, Some(labels)) => {
if precedence > 10 {
write!(f, "(")?;
}
write!(f, "{} {{", id.name)?;
for (i, (name, value)) in
labels.iter().zip(fields).enumerate()
{
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{name} = ")?;
value.print(f, 0)?;
}
write!(f, "}}")?;
if precedence > 10 {
write!(f, ")")?;
}
Ok(())
}
Self::Con(id, fields, None) => match id.name.as_str() {
"[]" => write!(f, "[]"),
":" => {
write!(f, "[")?;
let mut value = self;
let mut first = true;
while let Self::Con(c, fs, _) = value {
if c.name == "[]" {
break;
}
if !first {
write!(f, ",")?;
}
first = false;
fs[0].print(f, 0)?;
value = &fs[1];
}
write!(f, "]")
}
"()" | "(,)" | "(,,)" => {
write!(f, "(")?;
for (i, v) in fields.iter().enumerate() {
if i > 0 {
write!(f, ",")?;
}
v.print(f, 0)?;
}
write!(f, ")")
}
_ => {
let parens = precedence > 10 && !fields.is_empty();
if parens {
write!(f, "(")?;
}
write!(f, "{}", id.name)?;
for v in fields {
write!(f, " ")?;
v.print(f, 11)?;
}
if parens {
write!(f, ")")?;
}
Ok(())
}
},
}
}
}
pub fn normalize(n: i64) -> i64 {
(n << 1) >> 1
}
/// Rebuild a recursive group's closures from a shared immutable recipe. The
/// recipe captures the outer environment, never the closures themselves.
// ANCHOR: recursive-closures
fn recursive_values(bindings: Rc<Vec<Bind>>, env: Rc<Env>) -> Env {
let mut values = Vec::new();
for binding in bindings.iter() {
let mut rhs = &*binding.rhs;
while let Expr::TyLam(_, inner) = rhs {
rhs = inner;
}
let Expr::Lam(parameter, body) = rhs else {
unreachable!("recursive lambda invariant")
};
let value = Value::Closure {
parameter: parameter.clone(),
body: Rc::new(*body.clone()),
env: env.clone(),
recursive: Some(bindings.clone()),
};
values.push((binding.name.clone(), value));
}
values
}
fn bind_group(
group: &BindGroup,
env: &mut Env,
fuel: &mut usize,
program: &core::Program,
globals: &RefCell<Env>,
) -> Result<(), Failure> {
if group.recursive {
env.extend(recursive_values(
Rc::new(group.binds.clone()),
Rc::new(env.clone()),
));
} else {
for binding in &group.binds {
let value =
evaluate(&binding.rhs, env, fuel, program, globals)?;
env.push((binding.name.clone(), value));
}
}
Ok(())
}
fn evaluate(
expr: &Expr,
env: &Env,
fuel: &mut usize,
program: &core::Program,
globals: &RefCell<Env>,
) -> Result<Value, Failure> {
*fuel = fuel.checked_sub(1).ok_or("step limit exceeded")?;
match expr {
Expr::Lit(l) => Ok(Value::Lit(l.clone())),
Expr::TyLam(_, body) | Expr::TyApp(body, _) => {
evaluate(body, env, fuel, program, globals)
}
Expr::Global(n) => globals
.borrow()
.iter()
.find(|(id, _)| id.unique() == n.unique())
.map(|(_, v)| v.clone())
.ok_or_else(|| {
Failure::Runtime(format!(
"internal error: uninitialized global {n}"
))
}),
Expr::Var(n) => env
.iter()
.rev()
.find(|(name, _)| name == n)
.map(|(_, v)| v.clone())
.ok_or_else(|| {
Failure::Runtime(format!("internal error: unbound {n}"))
}),
Expr::Lam(parameter, body) => Ok(Value::Closure {
parameter: parameter.clone(),
body: Rc::new(*body.clone()),
env: Rc::new(env.clone()),
recursive: None,
}),
// ANCHOR: application
Expr::App(function, argument) => {
let function = evaluate(function, env, fuel, program, globals)?;
let argument = evaluate(argument, env, fuel, program, globals)?;
if let Value::Closure {
parameter,
body,
env,
recursive,
} = &function
{
let mut local = (**env).clone();
if let Some(bindings) = recursive {
local.extend(recursive_values(
bindings.clone(),
env.clone(),
));
}
local.push((parameter.clone(), argument));
evaluate(body, &local, fuel, program, globals)
} else {
Err("internal error: non-function application".into())
}
}
// ANCHOR_END: application
Expr::Let(group, body) => {
let mut local = env.clone();
bind_group(group, &mut local, fuel, program, globals)?;
evaluate(body, &local, fuel, program, globals)
}
Expr::Con(id, _, fields) => Ok(Value::Con(
id.clone(),
fields
.iter()
.map(|e| evaluate(e, env, fuel, program, globals))
.collect::<Result<_, _>>()?,
program.constructors[id.index as usize].labels.clone(),
)),
Expr::PatternFail(span, _) => Err(failure(program, *span).into()),
Expr::Match(m) => {
let values = m
.scruts
.iter()
.map(|id| {
evaluate(
&Expr::Var(id.clone()),
env,
fuel,
program,
globals,
)
})
.collect::<Result<Vec<_>, _>>()?;
for row in &m.rows {
let mut local = env.clone();
if !row
.pats
.iter()
.zip(&values)
.all(|(p, v)| matches_pattern(p, v, &mut local))
{
continue;
}
for group in &row.locals {
bind_group(group, &mut local, fuel, program, globals)?;
}
match &row.body {
core::RowBody::Plain(e) => {
return evaluate(e, &local, fuel, program, globals);
}
core::RowBody::Guarded(gs) => {
for (g, e) in gs {
let value = evaluate(
g, &local, fuel, program, globals,
)?;
if matches!(value,Value::Con(id,_,_) if id.name=="True")
{
return evaluate(
e, &local, fuel, program, globals,
);
}
}
}
}
}
Err(failure(program, m.span).into())
}
Expr::Prim(op, _, args, _) => {
let values = args
.iter()
.map(|a| evaluate(a, env, fuel, program, globals))
.collect::<Result<Vec<_>, _>>()?;
primitive(*op, &values, program, globals)
}
}
}
fn primitive(
op: Op,
args: &[Value],
program: &core::Program,
_globals: &RefCell<Env>,
) -> Result<Value, Failure> {
if let Op::Runtime(p) = op {
return crate::primitives::evaluate(p, args, program);
}
if let [Value::Lit(Lit::String(a)), Value::Lit(Lit::String(b))] = args
&& op == Op::Append
{
return Ok(Value::Lit(Lit::String(format!("{a}{b}"))));
}
let [Value::Lit(Lit::Int(a)), Value::Lit(Lit::Int(b))] = args else {
return Err("internal error: primitive operands".into());
};
if matches!(op, Op::Eq | Op::Lt | Op::Le) {
let yes = match op {
Op::Eq => a == b,
Op::Lt => a < b,
_ => a <= b,
};
let name = if yes { "True" } else { "False" };
let c = program
.constructors
.iter()
.find(|c| c.id.name == name)
.ok_or("internal error: missing Bool constructor")?;
return Ok(Value::Con(c.id.clone(), vec![], c.labels.clone()));
}
let value = match op {
Op::Runtime(p) => {
return crate::primitives::evaluate(p, args, program);
}
Op::Add => Lit::Int(normalize(a.wrapping_add(*b))),
Op::Sub => Lit::Int(normalize(a.wrapping_sub(*b))),
Op::Mul => Lit::Int(normalize(a.wrapping_mul(*b))),
Op::Eq | Op::Lt | Op::Le => unreachable!(),
Op::Div | Op::Mod => {
if *b == 0 {
return Err("divide by zero".into());
}
let (mut q, mut r) = (a / b, a % b);
if r != 0 && (r < 0) != (*b < 0) {
q -= 1;
r += b;
}
Lit::Int(normalize(if op == Op::Div { q } else { r }))
}
Op::Append => {
return Err("internal error: string append on Int".into());
}
};
Ok(Value::Lit(value))
}
/// Evaluate top-level values exactly once in dependency order, then select main's value.
fn execute(
program: &core::Program,
mut fuel: usize,
io: bool,
) -> Result<String, Failure> {
let globals = RefCell::new(vec![]);
for group in &program.groups {
for b in &group.binds {
if core::is_function(&b.rhs) {
let value = evaluate(
&b.rhs,
&vec![],
&mut fuel,
program,
&globals,
)?;
globals.borrow_mut().push((b.name.clone(), value));
}
}
}
for group in &program.groups {
for b in &group.binds {
if !core::is_function(&b.rhs) {
let value = evaluate(
&b.rhs,
&vec![],
&mut fuel,
program,
&globals,
)?;
globals.borrow_mut().push((b.name.clone(), value));
}
}
}
let value = globals
.borrow()
.iter()
.find(|(n, _)| n.unique() == program.main.unique())
.map(|(_, v)| v.clone())
.ok_or("internal error: missing main")?;
if io {
let unit = program
.constructors
.iter()
.find(|c| c.id.name == "()")
.unwrap();
let action = Expr::App(
Box::new(Expr::Global(program.main.clone())),
Box::new(Expr::Con(unit.id.clone(), vec![], vec![])),
);
evaluate(&action, &vec![], &mut fuel, program, &globals)?;
Ok(String::new())
} else {
Ok(value.to_string())
}
}
/// Evaluate a pure fragment; retained for compiler tests and REPL inspection.
pub fn run(program: &core::Program, fuel: usize) -> Result<String, String> {
execute(program, fuel, false).map_err(|e| e.to_string())
}
/// Initialize globals, then run the checked IO action exactly once.
pub fn run_io(program: &core::Program, fuel: usize) -> Result<(), Failure> {
execute(program, fuel, true).map(|_| ())
}
fn failure(program: &core::Program, span: crate::source::Span) -> String {
let site = program
.locations
.iter()
.find(|(s, _)| *s == span)
.map(|(_, s)| s.as_str())
.unwrap_or("<unknown>:1:1");
format!("pattern match failure at {site}")
}
fn matches_pattern(p: &core::Pat, v: &Value, env: &mut Env) -> bool {
match p {
core::Pat::Wild => true,
core::Pat::Var(id) => {
env.push((id.clone(), v.clone()));
true
}
core::Pat::As(id, p) => {
env.push((id.clone(), v.clone()));
matches_pattern(p, v, env)
}
core::Pat::Lit(Lit::Double(n)) => {
matches!(v,Value::Lit(Lit::Double(m)) if f64::from_bits(*n)==f64::from_bits(*m))
}
core::Pat::Lit(l) => matches!(v,Value::Lit(actual) if l==actual),
core::Pat::Con(id, ps) => match v {
Value::Con(actual, vs, _)
if id == actual && ps.len() == vs.len() =>
{
ps.iter().zip(vs).all(|(p, v)| matches_pattern(p, v, env))
}
_ => false,
},
}
}
/// Language termination is separate from an ordinary runtime failure.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Failure {
Runtime(String),
Exit(i32),
}
}
드라이버는 실행이 끝날 때와 런타임 진단을 출력하기 전에 stdout을 플러시합니다. 입력 프리미티브는 읽기 전에 플러시하므로, 프로그램이 블록되기 전에 프롬프트가 사용자에게 전달됩니다. 테스트 실행기는 .in 파일을 받아 각 엔진에 동일한 입력을 보냅니다. 파이프 핸드셰이크 회귀 테스트는 터미널 버퍼링과 무관하게 플러시를 검사합니다.
여기서 기존 프로그램은 약간 손봐야 합니다. 드라이버가 더 이상 main을 대신 출력해 주지 않으므로, 출력 코드를 프로그램 안에 직접 넣습니다. 값에 Show 인스턴스가 없다면 인스턴스를 직접 제공하거나 그 값에서 출력할 수 있는 부분을 골라야 합니다. 기존 값 프린터는 컴파일러 조각을 검사할 수 있도록 --print-value를 통해 계속 사용할 수 있습니다.
실행해 보기
저장소 루트에서 이 장의 크레이트로 들어가십시오. 다음 명령은 명시적인 액션의 출력과 추론된 진입점 타입을 출력합니다:
$ cd crates/ch15_io
$ cargo run -q -- run tests/programs/io/hello.fun --lint
hello
$ cargo run -q -- dump --pass=types tests/programs/io/hello.fun
main : IO ()
$ cargo run -q -- run tests/programs/io/deferred.fun --lint
ok
--pass=core를 사용하면 사전 인자와 unit을 받는 람다를 찾을 수 있습니다. 사용되지 않는 출력 액션과 main이 실제로 순차 실행하는 출력 액션을 비교해 볼 만합니다. 둘 다 값이지만, 본문이 실행되는 것은 후자뿐입니다. 이 장에서도 우리의 실행 엔진은 여전히 Core 인터프리터입니다.
연습 문제
- 준비 운동. 예제의 두 번째
Just를 적절히 타입을 표기한Nothing으로 바꾸십시오. 그 컨티뉴에이션이 호출되지 않는 이유를 설명하십시오. - 준비 운동. 사용되지 않는 출력 액션을 만드는 경우와, 문자열 인자가
error를 호출하는 출력 액션을 만드는 경우를 비교하십시오. - 중급.
let, 튜플 바인드, 섀도잉 바인드를 포함하는 do 블록에 대해 생성되는 스코프를 추적하십시오. - 중급. Core에서
Monad IO사전을 살펴보고 IO 낮추기(lowering)의 영향을 받는 모든 타입을 찾아내십시오. - 고급. 파이프 핸드셰이크 테스트를 확장하여 프롬프트를 두 번 내보내고, 터미널의 줄 버퍼링에 의존하지 않고 그 순서를 검증하십시오.
더 읽을거리
- 보고서의 do 표현식과 모나드 라이브러리 (Simon Marlow 2010).
- 고계 카인드 생성자 클래스 (Jones 1993).
- 사전 전달과 한정 타입 (Jones 1999).