以下是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ī)的思路,通過仔細(xì)構(gòu)建狀態(tài)轉(zhuǎn)移表茸歧,再將狀態(tài)轉(zhuǎn)移表用代碼進(jìn)行表述,能得到執(zhí)行效率頗高(O(n))的算法显沈。不過這種解法對(duì)代碼復(fù)用很不友好软瞎,稍微改下題目,例如"驗(yàn)證給定字符串是否可以解釋為復(fù)數(shù)"拉讯,就得重新構(gòu)建狀態(tài)轉(zhuǎn)移表涤浇,重新寫代碼。而且構(gòu)建狀態(tài)轉(zhuǎn)移表的過程是機(jī)械化魔慷、串行的只锭,與人類通過層層分解來(lái)處理復(fù)雜問題的習(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)解決文章開頭提到的面試題隧熙。
預(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一書中有一個(gè)簡(jiǎn)單而明確的解釋:
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è)過程有點(diǎn)像拼積木,使用簡(jiǎn)單的基本構(gòu)件讥脐,通過層層組合遭居,構(gòu)造出更大更復(fù)雜的系統(tǒng):
定義關(guān)鍵類型
函數(shù)式編程的要素之一是無(wú)副作用的”純“函數(shù)(pure function)。和數(shù)學(xué)函數(shù)一樣旬渠,這類函數(shù)的返回值俱萍,完全由調(diào)用者提供的參數(shù)決定,而且除了返回結(jié)果外告丢,不執(zhí)行其他任何帶副作用的操作枪蘑。相對(duì)的,”不純“的函數(shù)岖免,它的返回值可能依賴當(dāng)前時(shí)間這類定義不明確而且易變的狀態(tài)岳颇,可能執(zhí)行一些副作用(side effect),例如拋出異常颅湘、修改全局狀態(tài)等话侧。
定義出純函數(shù)的關(guān)鍵,在于明確輸入輸出的類型栅炒,做到不偏不倚掂摔,讓輸入包含函數(shù)執(zhí)行所需的全部信息,讓輸出體現(xiàn)全部可能的結(jié)果赢赊。
對(duì)于一個(gè)parser來(lái)說乙漓,輸入比較簡(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)該通過類型體現(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叮盘,不過java的類型系統(tǒng)不能優(yōu)雅地描述這種類型限定(sum type),需要我們?cè)谶\(yùn)行時(shí)做一些檢查以保證這種約束關(guān)系的成立霹俺。另外柔吼,為了方便地基于ParseResult進(jìn)行更進(jìn)一步的運(yùn)算,我們可以使用類成員函數(shù)的方式定義一些read類方法:
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
類似丙唧,parse result為fail的話愈魏,則什么都不做直接返回fail result,否則的話想际,將parse得到的value進(jìn)行進(jìn)一步轉(zhuǎn)換培漏。這類方法的共同點(diǎn)在于,用”高階函數(shù)“將重復(fù)出現(xiàn)的pattern(這里的pattern是傳播錯(cuò)誤沼琉,轉(zhuǎn)換結(jié)果)抽象成類型安全的API北苟,優(yōu)點(diǎn)是方便對(duì)其他函數(shù)進(jìn)行組合和減少重復(fù)代碼。
定義好輸入輸出類型后打瘪,parser的類型也就確定了:
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
類似的高階函數(shù)课兄。要定義合理的高階函數(shù)牍氛,則首先需要識(shí)別出計(jì)算中重復(fù)出現(xiàn)的pattern,然后將這些pattern進(jìn)行抽象烟阐。
例如搬俊,定義parser時(shí)踱稍,可能遇到的pattern包括:
- 使用parser<T>得到T類型的結(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類型的結(jié)果,然后再將T類型的結(jié)果轉(zhuǎn)換成類型為T1的值
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類型的結(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類型的結(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類型的結(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í)并沒有引入更深一層的抽象。之所以組這個(gè)封裝抚吠,是為了避免實(shí)際應(yīng)用中出現(xiàn)嵌套lambda常潮,影響代碼可讀性。理論上楷力,當(dāng)然還可以定義seq3喊式,seq4,seq5等封裝萧朝。而在Haskell等函數(shù)式語(yǔ)言中岔留,則通過do..return
語(yǔ)法糖來(lái)解決這一問題。
- 重復(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類似厕吉,但是要求必須至少parse成功一次
static <T> Parser<List<T>> more1(Parser<T> subParser) {
return more(subParser).filter(list -> !list.isEmpty());
}
這里不再羅列所有組合函數(shù)酱固,完整代碼見這個(gè)gist。
為數(shù)字解析定義值類型
為了完成接下來(lái)的數(shù)字解析器头朱,我們先自定義一些數(shù)據(jù)類型运悲,用來(lái)更清晰地表示各種數(shù)字的組成。
例如项钮,一組數(shù)字字符可以通過如下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)定義相關(guān)數(shù)據(jù)的字面構(gòu)成,而不沒有包含任何數(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)建常見的基礎(chǔ)parser
前面提到,函數(shù)式編程類似于搭積木媚创。我們已經(jīng)花了很大篇幅來(lái)定義積木之間組合的規(guī)則,這一步我們開始制作幾個(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ī)則声离,我們可以開始逐步構(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)本刽,我們就可以逐步開始構(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è)專門的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);
類似的烹看,正確的科學(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,不過原題中只需要驗(yàn)證是否可以解析得到正確的科學(xué)計(jì)數(shù)也殖,所以最終solution方法為:
public boolean isNumber(String s) {
return numberSurroundWithSpaces.parse(s).isSucceed();
}
總結(jié)
我們花了很大篇幅用來(lái)構(gòu)建一個(gè)簡(jiǎn)單的類parsec庫(kù)土思,然后在這個(gè)庫(kù)的基礎(chǔ)上,用聲明式語(yǔ)法構(gòu)建了一個(gè)科學(xué)計(jì)數(shù)解析器毕源。對(duì)于一道面試題來(lái)說浪漠,確實(shí)有大動(dòng)干戈的嫌疑(我最終AC的solution行數(shù)為400,執(zhí)行時(shí)間也排在末尾5%, 而推薦題解只有36行)霎褐。但實(shí)際上址愿,對(duì)于絕大部分現(xiàn)代程序員來(lái)說,日常工作中最大的挑戰(zhàn)冻璃,不是如何優(yōu)化代碼的執(zhí)行復(fù)雜度(怎么寫執(zhí)行更快)响谓,而是優(yōu)化代碼的邏輯復(fù)雜度(怎么寫更容易讀懂),即如何讓你的代碼不會(huì)比業(yè)務(wù)邏輯本身更復(fù)雜省艳,更難以理解娘纷。從這個(gè)角度考慮,函數(shù)式編程是我們的不二之選:它對(duì)副作用的謹(jǐn)慎對(duì)待讓我們更容易寫出安全跋炕、穩(wěn)定的代碼赖晶;它對(duì)高階函數(shù)的提倡,把依賴經(jīng)驗(yàn)口口相傳的設(shè)計(jì)模式落地到具體的代碼中辐烂,在類型系統(tǒng)的加持下遏插,我們可以順利地打造各個(gè)問題領(lǐng)域的樂高世界。