以下是leetcode上一道標(biāo)注為"困難"的題:
驗(yàn)證給定的字符串是否可以解釋為十進(jìn)制數(shù)字。
例如:
"0" => true
" 0.1 " => true
"abc" => false
"1 a" => false
"2e10" => true
" -90e3 " => true
" 1e" => false
"e3" => false
" 6e-1" => true
" 99e2.5 " => false
"53.5e93" => true
" --6 " => false
"-+3" => false
"95a54e53" => false
不同于算法題,它的難點(diǎn)不在于如何提高執(zhí)行效率,縮減執(zhí)行時(shí)間,而在于如何清晰、正確地完成"復(fù)雜"業(yè)務(wù)邏輯到代碼的轉(zhuǎn)換。參考題解大多使用了顯式維護(hù)狀態(tài)機(jī)的思路,通過(guò)仔細(xì)構(gòu)建狀態(tài)轉(zhuǎn)移表,再將狀態(tài)轉(zhuǎn)移表用代碼進(jìn)行表述,能得到執(zhí)行效率頗高(O(n))的算法。不過(guò)這種解法對(duì)代碼復(fù)用很不友好,稍微改下題目,例如"驗(yàn)證給定字符串是否可以解釋為復(fù)數(shù)",就得重新構(gòu)建狀態(tài)轉(zhuǎn)移表,重新寫(xiě)代碼。而且構(gòu)建狀態(tài)轉(zhuǎn)移表的過(guò)程是機(jī)械化、串行的,與人類(lèi)通過(guò)層層分解來(lái)處理復(fù)雜問(wèn)題的習(xí)慣相悖,這就導(dǎo)致對(duì)大部分人而言,構(gòu)建狀態(tài)轉(zhuǎn)移表枯燥而容易出錯(cuò)。
本文受haskell中的parsec庫(kù)啟發(fā),嘗試基于java8引入的函數(shù)式特性(lambda和method reference),先實(shí)現(xiàn)一個(gè)簡(jiǎn)陋的java版parsec,然后使用這個(gè)庫(kù)定義一組number parser,用來(lái)解決文章開(kāi)頭提到的面試題。
預(yù)覽
簡(jiǎn)而言之,我們最終將得到一組parser庫(kù),在這個(gè)庫(kù)的基礎(chǔ)上,可以使用聲明式語(yǔ)法構(gòu)建具體的文本解析器。最終用來(lái)解析leetcode 面試題的parser可以按如下方式構(gòu)建:
// 無(wú)符號(hào)整數(shù)
public static final Parser<Int> unsignedInt = digits1plus.map(Int::unsigned);
// 有符號(hào)整數(shù)
public static final Parser<Int> signedInt = seq(flag, unsignedInt, Int::signed);
// 包含整數(shù)部分的無(wú)符號(hào)浮點(diǎn)數(shù)
public static final Parser<Float> fullUnsignedFloat =
seq(digits1plus, point.then(digits0plus), Float::full);
// 不含整數(shù)部分的無(wú)符號(hào)浮點(diǎn)數(shù)
public static final Parser<Float> onlyFractionPartUnsignedFloat =
point.then(digits1plus).map(Float::onlyFractionPart);
// 只含整數(shù)部分的無(wú)符號(hào)浮點(diǎn)數(shù)
public static final Parser<Float> onlyIntPartUnsignedFloat =
digits1plus.map(Float::onlyIntPart);
// 任意無(wú)符號(hào)浮點(diǎn)數(shù)
public static final Parser<Float> unsignedFloat =
fullUnsignedFloat.or(onlyFractionPartUnsignedFloat).or(onlyIntPartUnsignedFloat);
// 任意有符號(hào)浮點(diǎn)數(shù)
public static final Parser<Float> floatParser = seq(flag, unsignedFloat, Float::signed);
// 包含指數(shù)部分的科學(xué)計(jì)數(shù)
public static final Parser<Number> withExpNumber = seq(floatParser, expFlag.then(signedInt),
Number::full);
// 不包含指數(shù)部分的科學(xué)計(jì)數(shù)
public static final Parser<Number> justFloatNumber = floatParser.map(Number::justFloat);
// 任意科學(xué)計(jì)數(shù)
public static final Parser<Number> number = withExpNumber.or(justFloatNumber);
// 前后為空白的任意科學(xué)計(jì)數(shù), 用于解決leetcode面試題
public static final Parser<Number> numberSurroundWithSpaces = number.surroundWith(spaces);
怎么實(shí)現(xiàn)?
我們將使用函數(shù)式編程的思想,構(gòu)建一個(gè)簡(jiǎn)易的適合組合的parser庫(kù)。關(guān)于函數(shù)式編程,在scala with cats一書(shū)中有一個(gè)簡(jiǎn)單而明確的解釋?zhuān)?/p>
This means designing systems as small composable units, expressing constraints and interactions via the type system, and using composition to guide the construction of large systems in a way that maintains the original architectural vision.
這個(gè)過(guò)程有點(diǎn)像拼積木,使用簡(jiǎn)單的基本構(gòu)件,通過(guò)層層組合,構(gòu)造出更大更復(fù)雜的系統(tǒng):

定義關(guān)鍵類(lèi)型
函數(shù)式編程的要素之一是無(wú)副作用的”純“函數(shù)(pure function)。和數(shù)學(xué)函數(shù)一樣,這類(lèi)函數(shù)的返回值,完全由調(diào)用者提供的參數(shù)決定,而且除了返回結(jié)果外,不執(zhí)行其他任何帶副作用的操作。相對(duì)的,”不純“的函數(shù),它的返回值可能依賴(lài)當(dāng)前時(shí)間這類(lèi)定義不明確而且易變的狀態(tài),可能執(zhí)行一些副作用(side effect),例如拋出異常、修改全局狀態(tài)等。
定義出純函數(shù)的關(guān)鍵,在于明確輸入輸出的類(lèi)型,做到不偏不倚,讓輸入包含函數(shù)執(zhí)行所需的全部信息,讓輸出體現(xiàn)全部可能的結(jié)果。
對(duì)于一個(gè)parser來(lái)說(shuō),輸入比較簡(jiǎn)單,就是一個(gè)字符串流:
interface CharStream {
/**
* 是否有更多字符等待解析
*/
boolean hasMore();
/**
* 讀取第一個(gè)字符(如果有的話)
*/
char peek();
/**
* 在當(dāng)前字符流中前進(jìn)一步
*/
CharStream advance();
}
輸出則復(fù)雜一點(diǎn),parse可能失敗,也可能成功,parse之后,字符流可能被消費(fèi)了若干個(gè)字符。這些都應(yīng)該通過(guò)類(lèi)型體現(xiàn)出來(lái):
class ParseResult<T> {
private final Throwable error;
private final T value;
private final CharStream stream;
ParseResult(Throwable error, T value, CharStream stream) {
this.error = error;
this.value = value;
this.stream = stream;
}
public static <T> ParseResult<T> success(T value, CharStream stream) {
Objects.requireNonNull(stream);
return new ParseResult<>(null, value, stream);
}
public static <T> ParseResult<T> error(Throwable error, CharStream stream) {
Objects.requireNonNull(stream);
return new ParseResult<>(error, null, stream);
}
}
這里用了泛型來(lái)描述parse后得到的值,以表示可以從字符流中解析得到各種結(jié)構(gòu)化數(shù)據(jù),而不只是題目中提到的數(shù)字。值得注意的是,ParserResult中error和value其實(shí)是互斥的,兩者必定一個(gè)為null,一個(gè)為非null,不過(guò)java的類(lèi)型系統(tǒng)不能優(yōu)雅地描述這種類(lèi)型限定(sum type),需要我們?cè)谶\(yùn)行時(shí)做一些檢查以保證這種約束關(guān)系的成立。另外,為了方便地基于ParseResult進(jìn)行更進(jìn)一步的運(yùn)算,我們可以使用類(lèi)成員函數(shù)的方式定義一些read類(lèi)方法:
public Throwable getError() {
return error;
}
public T getValue() {
return value;
}
public CharStream getStream() {
return stream;
}
public <T1> ParseResult<T1> valueMap(Function<T, T1> mapper) {
if (isFailed()) {
@SuppressWarnings("unchecked")
ParseResult<T1> valueMappedParseResult = (ParseResult<T1>) this;
return valueMappedParseResult;
} else {
return success(mapper.apply(value), stream);
}
}
public ParseResult<T> onError(Consumer<Throwable> consumer) {
if (isFailed()) {
consumer.accept(error);
}
return this;
}
public ParseResult<T> onValue(Consumer<T> consumer) {
if (isSucceed()) {
consumer.accept(value);
}
return this;
}
其中valueMap和java 8中的Optional.map類(lèi)似,parse result為fail的話,則什么都不做直接返回fail result,否則的話,將parse得到的value進(jìn)行進(jìn)一步轉(zhuǎn)換。這類(lèi)方法的共同點(diǎn)在于,用”高階函數(shù)“將重復(fù)出現(xiàn)的pattern(這里的pattern是傳播錯(cuò)誤,轉(zhuǎn)換結(jié)果)抽象成類(lèi)型安全的API,優(yōu)點(diǎn)是方便對(duì)其他函數(shù)進(jìn)行組合和減少重復(fù)代碼。
定義好輸入輸出類(lèi)型后,parser的類(lèi)型也就確定了:
interface Parser<T> {
ParseResult<T> parse(CharStream stream);
}
這種只含一個(gè)抽象方法的interface在java 8中叫做functional interface。它既可以由普通class實(shí)現(xiàn),也可以由method reference和lambda 表達(dá)式實(shí)現(xiàn)。正是這一特性,讓我們可以在java這種函數(shù)并非一等公民的語(yǔ)言中,比較優(yōu)雅地實(shí)現(xiàn)函數(shù)式編程。
處理parser之間的組合
函數(shù)式編程的要素之二,是能夠?qū)瘮?shù)方便、安全地進(jìn)行組合,這樣我們才能像搭積木一樣,從簡(jiǎn)單、堅(jiān)實(shí)的基礎(chǔ)組件出發(fā),構(gòu)建更大、更復(fù)雜、功能更豐富的系統(tǒng),同時(shí)保證這個(gè)大的系統(tǒng)和基礎(chǔ)組件一樣堅(jiān)實(shí)、穩(wěn)定。
而要實(shí)現(xiàn)對(duì)函數(shù)方便、安全地進(jìn)行組合,關(guān)鍵在于定義和前面提到的valueMap類(lèi)似的高階函數(shù)。要定義合理的高階函數(shù),則首先需要識(shí)別出計(jì)算中重復(fù)出現(xiàn)的pattern,然后將這些pattern進(jìn)行抽象。
例如,定義parser時(shí),可能遇到的pattern包括:
- 使用parser<T>得到T類(lèi)型的結(jié)果,然后判斷結(jié)果是否符合某種特征,不符合的話,就返回解析失敗
default Parser<T> filter(Predicate<T> predicate) {
return stream -> {
ParseResult<T> result = parse(stream);
if (result.isFailed()) {
return ParseResult.error(result.getError(), stream);
} else {
T value = result.getValue();
if (predicate.test(value)) {
return result;
} else {
return ParseResult.error(
new Throwable(String.format("%s not match against predicate", value)), stream);
}
}
};
}
- 使用parser<T>得到T類(lèi)型的結(jié)果,然后再將T類(lèi)型的結(jié)果轉(zhuǎn)換成類(lèi)型為T(mén)1的值
default <T1> Parser<T1> map(Function<T, T1> mapper) {
return stream -> parse(stream).valueMap(mapper);
}
- 先使用parser 1解析,成功的話直接返回結(jié)果,失敗了再使用parser 2解析
default Parser<T> or(Parser<T> recoverParser) {
return stream -> {
ParseResult<T> result = parse(stream);
if (result.isFailed()) {
return recoverParser.parse(stream);
} else {
return result;
}
};
}
- 使用parser<T>得到T類(lèi)型的結(jié)果,然后基于結(jié)果t決定如何解析字符流的其余部分
default <T1> Parser<T1> flatMap(Function<T, Parser<T1>> mapper) {
return stream -> {
ParseResult<T> result = parse(stream);
if (result.isFailed()) {
return (ParseResult<T1>) result;
} else {
Parser<T1> nextParser = mapper.apply(result.getValue());
return nextParser.parse(result.getStream());
}
};
}
- 使用parser<T>得到T類(lèi)型的結(jié)果,失敗的話,返回解析失??;成功的話,用parser<T1>解析剩余的字符流,但是T結(jié)果棄之不用。
default <T1> Parser<T1> then(Parser<T1> parser) {
return stream -> {
ParseResult<T> result = parse(stream);
if (result.isFailed()) {
return (ParseResult<T1>) result;
} else {
return parser.parse(result.getStream());
}
};
}
- 使用parser<T1>得到T類(lèi)型的結(jié)果,成功的話,繼續(xù)使用parser<T2>解析剩余的字節(jié)流,最后基于結(jié)果T1和T2構(gòu)建最終結(jié)果T3
static <T1, T2, T3> Parser<T3> seq(Parser<T1> parser1, Parser<T2> parser2,
BiFunction<T1, T2, T3> merger) {
return parser1.flatMap(t1 -> parser2.map(t2 -> merger.apply(t1, t2)));
}
這個(gè)定義基本上只是flatMap和map的一個(gè)封裝,其實(shí)并沒(méi)有引入更深一層的抽象。之所以組這個(gè)封裝,是為了避免實(shí)際應(yīng)用中出現(xiàn)嵌套lambda,影響代碼可讀性。理論上,當(dāng)然還可以定義seq3,seq4,seq5等封裝。而在Haskell等函數(shù)式語(yǔ)言中,則通過(guò)do..return語(yǔ)法糖來(lái)解決這一問(wèn)題。
- 重復(fù)使用parser<T>進(jìn)行解析,然后將結(jié)果收集到一個(gè)list中。list長(zhǎng)度和parse成功的次數(shù)相同。
static <T> Parser<List<T>> more(Parser<T> subParser) {
return stream -> {
List<T> results = new ArrayList<>();
ParseResult<T> result = subParser.parse(stream);
while (result.isSucceed()) {
results.add(result.getValue());
stream = result.getStream();
result = subParser.parse(stream);
}
return ParseResult.success(results, stream);
};
}
- 和7類(lèi)似,但是要求必須至少parse成功一次
static <T> Parser<List<T>> more1(Parser<T> subParser) {
return more(subParser).filter(list -> !list.isEmpty());
}
這里不再羅列所有組合函數(shù),完整代碼見(jiàn)這個(gè)gist。
為數(shù)字解析定義值類(lèi)型
為了完成接下來(lái)的數(shù)字解析器,我們先自定義一些數(shù)據(jù)類(lèi)型,用來(lái)更清晰地表示各種數(shù)字的組成。
例如,一組數(shù)字字符可以通過(guò)如下class表示:
class Digits {
static final Digits empty = new Digits(Collections.emptyList());
final List<Character> digits;
Digits(List<Character> digits) {
this.digits = digits;
}
@Override
public String toString() {
return digits.stream().map(String::valueOf).collect(Collectors.joining());
}
public boolean isEmpty() {
return digits.isEmpty();
}
}
一個(gè)整數(shù),由一個(gè)符號(hào)位,和一組數(shù)字字符組成:
class Int {
static final Int empty = unsigned(Digits.empty);
final Character flag;
final Digits digits;
public Int(Character flag, Digits digits) {
this.flag = flag;
this.digits = digits;
}
static Int unsigned(Digits digits) {
return new Int(null, digits);
}
static Int signed(Character flag, Int unsigned) {
return new Int(flag, unsigned.digits);
}
@Override
public String toString() {
return flag == null ? digits.toString() : flag + digits.toString();
}
public boolean isEmpty() {
return digits.isEmpty();
}
}
一個(gè)形如-3.14156的浮點(diǎn)數(shù),可以認(rèn)為由三部分組成-符號(hào)位,小數(shù)點(diǎn)前的整數(shù)部分,小數(shù)點(diǎn)后的部分:
class Float {
final Character flag;
final Digits intPart;
final Digits fractionPart;
public Float(Character flag, Digits intPart, Digits fractionPart) {
this.flag = flag;
this.intPart = intPart;
this.fractionPart = fractionPart;
}
static Float full(Digits intPart, Digits fractionPart) {
assert !intPart.isEmpty();
assert !fractionPart.isEmpty();
return new Float(null, intPart, fractionPart);
}
static Float onlyIntPart(Digits intPart) {
assert !intPart.isEmpty();
return new Float(null, intPart, Digits.empty);
}
static Float onlyFractionPart(Digits fractionPart) {
return new Float(null, Digits.empty, fractionPart);
}
static Float signed(Character flag, Float unsigned) {
return new Float(flag, unsigned.intPart, unsigned.fractionPart);
}
@Override
public String toString() {
String formattedUnsignedPart = String.format("%s.%s", intPart, fractionPart);
return flag == null ? formattedUnsignedPart : flag + formattedUnsignedPart;
}
public boolean isEmpty() {
return intPart.isEmpty() && fractionPart.isEmpty();
}
}
而一個(gè)形如-3.1415E10的科學(xué)計(jì)數(shù),則可以認(rèn)為由兩個(gè)部分組成-浮點(diǎn)數(shù)部分,以及E后面的指數(shù)部分:
class Number {
final Float floatPart;
final Int expPart;
Number(Float floatPart, Int expPart) {
this.floatPart = floatPart;
this.expPart = expPart;
}
static Number full(Float floatPart, Int expPart) {
assert !floatPart.isEmpty();
assert !expPart.isEmpty();
return new Number(floatPart, expPart);
}
static Number justFloat(Float floatPart) {
assert !floatPart.isEmpty();
return new Number(floatPart, Int.empty);
}
@Override
public String toString() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(floatPart);
if (expPart != null) {
stringBuilder.append('e').append(expPart);
}
return stringBuilder.toString();
}
}
注意,Int、Float、Number等自定義類(lèi)型,主要是用來(lái)定義相關(guān)數(shù)據(jù)的字面構(gòu)成,而不沒(méi)有包含任何數(shù)值運(yùn)算的部分。當(dāng)然,要用這些class的實(shí)例做數(shù)值計(jì)算也簡(jiǎn)單,給每個(gè)class都實(shí)現(xiàn)一個(gè)到double或long的轉(zhuǎn)換函數(shù)就好。
到這里,一個(gè)簡(jiǎn)易的parser combinator library就算搭建完成了。接下來(lái),我們可以轉(zhuǎn)移到”應(yīng)用層“-構(gòu)建數(shù)字解析器。
構(gòu)建常見(jiàn)的基礎(chǔ)parser
前面提到,函數(shù)式編程類(lèi)似于搭積木。我們已經(jīng)花了很大篇幅來(lái)定義積木之間組合的規(guī)則,這一步我們開(kāi)始制作幾個(gè)基礎(chǔ)積木,也就是無(wú)法基于其他parser組合得到的基礎(chǔ)parser。
- 提取任意單個(gè)字符的parser
public static final Parser<Character> one = stream -> {
if (!stream.hasMore()) {
return ParseResult.error(new Throwable("no more characters"), stream);
} else {
return ParseResult.success(stream.peek(), stream.advance());
}
};
- 匹配空字符流/字符流末尾的parser
public static Parser<Void> eof = stream -> {
if (stream.hasMore()) {
return ParseResult.error(new Throwable("there are more characters"), stream);
} else {
return ParseResult.success(null, stream);
}
};
組合
有了前面提到的兩個(gè)基礎(chǔ)parser,以及前面的組合規(guī)則,我們可以開(kāi)始逐步構(gòu)造一些”更實(shí)用“的簡(jiǎn)單parser了。例如:
- 單個(gè)數(shù)字/多個(gè)數(shù)字
public static Parser<Character> digit = one.filter(ch -> ch >= '0' && ch <= '9');
public static final Parser<Digits> digits0plus = more(digit).map(Digits::new);
public static final Parser<Digits> digits1plus = more1(digit).map(Digits::new);
- 單個(gè)空格/多個(gè)空格
public static Parser<Character> space = one.filter(eq(' '));
public static Parser<List<Character>> spaces = more(space);
- 正負(fù)符號(hào)
public static final Parser<Character> positiveFlag = one.filter(eq('+'));
public static final Parser<Character> negativeFlag = one.filter(eq('-'));
public static final Parser<Character> flag = positiveFlag.or(negativeFlag).or(just(null));
- 小數(shù)點(diǎn)
public static final Parser<Character> point = one.filter(eq('.'));
- e指數(shù)符號(hào)
public static final Parser<Character> expFlag = one.filter(ch -> ch == 'e' || ch == 'E');
再組合
有了數(shù)字,正負(fù)符號(hào),小數(shù)點(diǎn),我們就可以逐步開(kāi)始構(gòu)建無(wú)符號(hào)整數(shù)->整數(shù)->浮點(diǎn)數(shù)->科學(xué)計(jì)數(shù)啦:
無(wú)符號(hào)整數(shù)的語(yǔ)法很簡(jiǎn)單,一到多個(gè)數(shù)字字符連續(xù)排列即可:
public static final Parser<Int> unsignedInt = digits1plus.map(Int::unsigned);
如果你想嚴(yán)格一點(diǎn),要求第一個(gè)字符不能為0的話,改動(dòng)也比較簡(jiǎn)單:
public static final Parser<Int> unsignedInt = digits1plus.filter(digits -> digits.first() != '0').map(Int::unsigned);
有符號(hào)整數(shù),則是符號(hào)位后面緊跟一個(gè)無(wú)符號(hào)整數(shù):
public static final Parser<Int> signedInt = seq(flag, unsignedInt, Int::signed);
無(wú)符號(hào)浮點(diǎn)數(shù)的情況比無(wú)符號(hào)整數(shù)復(fù)雜一點(diǎn),需要考慮是否包含小數(shù)點(diǎn),有小數(shù)點(diǎn)的話,要考慮小數(shù)點(diǎn)前是否有數(shù)字。一個(gè)方法是給每種情況定義一個(gè)專(zhuān)門(mén)的parser,然后or組合構(gòu)成最終的無(wú)符號(hào)浮點(diǎn)數(shù)parser:
// 包含整數(shù)部分的無(wú)符號(hào)浮點(diǎn)數(shù)
public static final Parser<Float> fullUnsignedFloat =
seq(digits1plus, point.then(digits0plus), Float::full);
// 不含整數(shù)部分的無(wú)符號(hào)浮點(diǎn)數(shù)
public static final Parser<Float> onlyFractionPartUnsignedFloat =
point.then(digits1plus).map(Float::onlyFractionPart);
// 只含整數(shù)部分的無(wú)符號(hào)浮點(diǎn)數(shù)
public static final Parser<Float> onlyIntPartUnsignedFloat =
digits1plus.map(Float::onlyIntPart);
// 任意無(wú)符號(hào)浮點(diǎn)數(shù)
public static final Parser<Float> unsignedFloat =
fullUnsignedFloat.or(onlyFractionPartUnsignedFloat).or(onlyIntPartUnsignedFloat);
類(lèi)似的,正確的科學(xué)計(jì)數(shù)可能只包含浮點(diǎn)數(shù)部分,也可能由浮點(diǎn)數(shù)部分和指數(shù)部分共同組成:
// 包含指數(shù)部分的科學(xué)計(jì)數(shù)
public static final Parser<Number> withExpNumber = seq(floatParser, expFlag.then(signedInt),
Number::full);
// 不包含指數(shù)部分的科學(xué)計(jì)數(shù)
public static final Parser<Number> justFloatNumber = floatParser.map(Number::justFloat);
// 任意科學(xué)計(jì)數(shù)
public static final Parser<Number> number = withExpNumber.or(justFloatNumber);
leetcode的題目中,允許數(shù)字前后出現(xiàn)任意多個(gè)空格:
public static final Parser<Number> numberSurroundWithSpaces = number.surroundWith(spaces);
至此,我們就完成了一個(gè)用于求解原面試題的數(shù)字parser,不過(guò)原題中只需要驗(yàn)證是否可以解析得到正確的科學(xué)計(jì)數(shù),所以最終solution方法為:
public boolean isNumber(String s) {
return numberSurroundWithSpaces.parse(s).isSucceed();
}
總結(jié)
我們花了很大篇幅用來(lái)構(gòu)建一個(gè)簡(jiǎn)單的類(lèi)parsec庫(kù),然后在這個(gè)庫(kù)的基礎(chǔ)上,用聲明式語(yǔ)法構(gòu)建了一個(gè)科學(xué)計(jì)數(shù)解析器。對(duì)于一道面試題來(lái)說(shuō),確實(shí)有大動(dòng)干戈的嫌疑(我最終AC的solution行數(shù)為400,執(zhí)行時(shí)間也排在末尾5%, 而推薦題解只有36行)。但實(shí)際上,對(duì)于絕大部分現(xiàn)代程序員來(lái)說(shuō),日常工作中最大的挑戰(zhàn),不是如何優(yōu)化代碼的執(zhí)行復(fù)雜度(怎么寫(xiě)執(zhí)行更快),而是優(yōu)化代碼的邏輯復(fù)雜度(怎么寫(xiě)更容易讀懂),即如何讓你的代碼不會(huì)比業(yè)務(wù)邏輯本身更復(fù)雜,更難以理解。從這個(gè)角度考慮,函數(shù)式編程是我們的不二之選:它對(duì)副作用的謹(jǐn)慎對(duì)待讓我們更容易寫(xiě)出安全、穩(wěn)定的代碼;它對(duì)高階函數(shù)的提倡,把依賴(lài)經(jīng)驗(yàn)口口相傳的設(shè)計(jì)模式落地到具體的代碼中,在類(lèi)型系統(tǒng)的加持下,我們可以順利地打造各個(gè)問(wèn)題領(lǐng)域的樂(lè)高世界。