一道數(shù)字解析面試題的函數(shù)式解法

以下是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ù)式特性(lambdamethod 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 referencelambda 表達(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包括:

  1. 使用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);
        }
      }
    };
  }
  1. 使用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);
  }
  1. 先使用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;
      }
    };
  }
  1. 使用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());
      }
    };
  }
  1. 使用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());
      }
    };
  }
  1. 使用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)題。

  1. 重復(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);
    };
  }
  1. 和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。

  1. 提取任意單個(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());
    }
  };
  1. 匹配空字符流/字符流末尾的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了。例如:

  1. 單個(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);
  1. 單個(gè)空格/多個(gè)空格
  public static Parser<Character> space = one.filter(eq(' '));
  public static Parser<List<Character>> spaces = more(space);
  1. 正負(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));
  1. 小數(shù)點(diǎn)
   public static final Parser<Character> point = one.filter(eq('.'));
  1. 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è)高世界。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容