一道數(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ī)的思路,通過仔細(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ù)式特性(lambdamethod 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 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類似的高階函數(shù)课兄。要定義合理的高階函數(shù)牍氛,則首先需要識(shí)別出計(jì)算中重復(fù)出現(xiàn)的pattern,然后將這些pattern進(jìn)行抽象烟阐。

例如搬俊,定義parser時(shí)踱稍,可能遇到的pattern包括:

  1. 使用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);
        }
      }
    };
  }
  1. 使用parser<T>得到T類型的結(jié)果,然后再將T類型的結(jié)果轉(zhuǎn)換成類型為T1的值
 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類型的結(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類型的結(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類型的結(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)解決這一問題。

  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類似厕吉,但是要求必須至少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钞钙。

  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ī)則声离,我們可以開始逐步構(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)本刽,我們就可以逐步開始構(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)域的樂高世界。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末纠修,一起剝皮案震驚了整個(gè)濱河市钧忽,隨后出現(xiàn)的幾起案子茎刚,更是在濱河造成了極大的恐慌怜校,老刑警劉巖氨鹏,帶你破解...
    沈念sama閱讀 211,265評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異辰妙,居然都是意外死亡鹰祸,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門上岗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)福荸,“玉大人,你說我怎么就攤上這事肴掷【慈瘢” “怎么了背传?”我有些...
    開封第一講書人閱讀 156,852評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)台夺。 經(jīng)常有香客問我径玖,道長(zhǎng),這世上最難降的妖魔是什么颤介? 我笑而不...
    開封第一講書人閱讀 56,408評(píng)論 1 283
  • 正文 為了忘掉前任梳星,我火速辦了婚禮,結(jié)果婚禮上滚朵,老公的妹妹穿的比我還像新娘冤灾。我一直安慰自己,他們只是感情好辕近,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評(píng)論 5 384
  • 文/花漫 我一把揭開白布韵吨。 她就那樣靜靜地躺著,像睡著了一般移宅。 火紅的嫁衣襯著肌膚如雪归粉。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,772評(píng)論 1 290
  • 那天漏峰,我揣著相機(jī)與錄音糠悼,去河邊找鬼。 笑死浅乔,一個(gè)胖子當(dāng)著我的面吹牛倔喂,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播靖苇,決...
    沈念sama閱讀 38,921評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼滴劲,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了顾复?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,688評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤鲁捏,失蹤者是張志新(化名)和其女友劉穎芯砸,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體给梅,經(jīng)...
    沈念sama閱讀 44,130評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡假丧,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了动羽。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片包帚。...
    茶點(diǎn)故事閱讀 38,617評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖运吓,靈堂內(nèi)的尸體忽然破棺而出渴邦,到底是詐尸還是另有隱情疯趟,我是刑警寧澤,帶...
    沈念sama閱讀 34,276評(píng)論 4 329
  • 正文 年R本政府宣布谋梭,位于F島的核電站信峻,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏瓮床。R本人自食惡果不足惜盹舞,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望隘庄。 院中可真熱鬧踢步,春花似錦、人聲如沸丑掺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)吼鱼。三九已至蓬豁,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間菇肃,已是汗流浹背地粪。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留琐谤,地道東北人蟆技。 一個(gè)月前我還...
    沈念sama閱讀 46,315評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像斗忌,于是被迫代替她去往敵國(guó)和親质礼。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評(píng)論 2 348