《Real World Haskell》中splitLines函数的表达式求值逻辑咨询
break Evaluation and splitLines Execution in Haskell Great question—let’s unpack this step by step, since lazy evaluation and Haskell’s pattern matching rules are key to understanding what’s happening here.
1. When does break’s result get evaluated?
Haskell uses lazy evaluation, which means expressions are only computed when their values are actually needed. The break isLineTerminator cs call returns a tuple (pre, suf), but this tuple isn’t fully evaluated the moment you bind it in the let clause.
Instead:
- If you need the value of
pre(like when we use it as the head of the result list),breakwill run just far enough to collect all characters until the first line terminator, then stop. It won’t touch thesufpart yet. - If you later need
suf(like in thecaseexpression),breakwill have already captured that remaining portion of the string, so accessing it just uses the already-determined suffix—breakonly traverses the string once total.
2. Execution flow of your splitLines function
Let’s walk through your code line by line, using the full (assumed) definition:
splitLines [] = [] splitLines cs = let (pre, suf) = break isLineTerminator cs in pre : case suf of ('\r':'\n':rest) -> splitLines rest ('\r':rest) -> splitLines rest ('\n':rest) -> splitLines rest _ -> []
Here’s the step-by-step execution when you call splitLines on a non-empty string:
- Bind the tuple (but don’t compute it yet): The
letclause creates a binding for(pre, suf)to the result ofbreak, but no computation happens here—this is just a reference to the expression. - Trigger
breakto getpre: The expressionpre : case ...needs to build a list. In Haskell, to construct a listx : xs, you first need to know the value ofx(the head). So we needprenow. This triggersbreakto run: it scanscsuntil it hits the first line terminator, returnspreas the substring up to that point, andsufas the rest of the string starting with the terminator. - Process the suffix with
case: Now thatpreis known (and the first element of the result list is set), we need to compute the rest of the list—the result of thecase suf ofexpression. This is when we actually inspectsuf:- If
sufstarts with\r\n, we skip both characters and recursively callsplitLineson the remaining string. - If it starts with just
\ror\n, we skip that single character and recurse. - If
sufis empty (no more line terminators), we return an empty list to end the result list.
- If
- Recurse until done: Each recursive call to
splitLinesrepeats this same process: bind the tuple, triggerbreakto get the next line’spre, then process the suffix.
To answer your specific question: You don’t "execute the let clause first" in a strict, eager way. The let binding is just a definition—computation only starts when parts of the tuple are needed. The pre gets evaluated first (to build the list head), then suf gets evaluated when the case expression needs to match against it.
Example walkthrough with "hello\r\nworld"
- First call:
splitLines "hello\r\nworld"breakruns until it hits\r, returningpre = "hello",suf = "\r\nworld"case sufmatches('\r':'\n':rest)whererest = "world", so we recurse withsplitLines "world"
- Second call:
splitLines "world"breakscans the entire string (no line terminators), sopre = "world",suf = []case sufmatches the_clause, returns[]
- Final result:
["hello", "world"]
内容的提问来源于stack exchange,提问作者Bercovici Adrian

