【问题标题】:How to easily read lines from stdin?如何轻松地从标准输入读取行?
【发布时间】:2016-11-23 19:16:23
【问题描述】:

前段时间,我决定在 HackerRank 上解决一个简单的任务,但使用 OCaml 和 Core 来学习它们。在其中一项任务中,我应该从标准输入中读取数据:

第一行包含一个整数,表示条目数 在电话簿中。随后的每一行都描述了一个条目 单行上的空格分隔值的形式。第一个值 是朋友的名字,第二个值是 -digit 电话号码。

在电话簿条目的行之后,有一个未知数量的 查询行。每行(查询)都包含一个要查找的内容,而您 必须继续读取行,直到没有更多输入为止。

主要问题:

  • 不知道会有多少行
  • 最后一行不以换行符结尾,所以在End_of_file之前我不能只阅读scanf "%s\n"

然后我的代码变得乱七八糟:

open Core.Std
open Printf
open Scanf


let read_numbers n =
    let phone_book = String.Table.create () ~size:n in

    for i = 0 to (n - 1) do
        match In_channel.input_line stdin with
        | Some line -> (
            match (String.split line ~on:' ') with
            | key :: data :: _ -> Hashtbl.set phone_book ~key ~data
            | _ -> failwith "This shouldn't happen"
        )
        | None -> failwith "This shouldn't happen"
    done;

    phone_book


let () =
    let rec loop phone_book =
        match In_channel.input_line stdin with
        | Some line -> (
            let s = match Hashtbl.find phone_book line with
                | Some number -> sprintf "%s=%s" line number
                | None -> "Not found"
            in
            printf "%s\n%!" s;
            loop phone_book
        )
        | None -> ()
    in

    match In_channel.input_line stdin with
    | Some n -> (
        let phone_book = read_numbers (int_of_string n) in
        loop phone_book
    )
    | None -> failwith "This shouldn't happen"

如果我用 Python 解决这个任务,那么代码如下所示:

n = int(input())
book = dict([tuple(input().split(' ')) for _ in range(n)])

while True:
    try:
        name = input()
    except EOFError:
        break
    else:
        if name in book:
            print('{}={}'.format(name, book[name]))
        else:
            print('Not found')

这比 OCaml 代码更短更清晰。关于如何改进我的 OCaml 代码的任何建议?还有两件重要的事情:我不想放弃 OCaml,我只想学习它;第二——出于同样的原因,我想使用 Core。

【问题讨论】:

    标签: ocaml


    【解决方案1】:

    OCaml 中 Python 代码的直接实现如下所示:

    let exec name =
      In_channel.(with_file name ~f:input_lines) |> function
      | [] -> invalid_arg "Got empty file"
      | x :: xs ->
        let es,qs = List.split_n xs (Int.of_string x) in
        let es = List.map es ~f:(fun entry -> match String.split ~on:' ' entry with
            | [name; phone] -> name,phone
            | _ -> invalid_arg "bad entry format") in
        List.iter qs ~f:(fun name ->
            match List.Assoc.find es name with
            | None -> printf "Not found\n"
            | Some phone -> printf "%s=%s\n" name phone)
    

    但是,OCaml 不是用于编写小脚本和一次性原型的脚本语言。它是编写真实软件的语言,必须是可读的、可支持的、可测试的和可维护的。这就是我们有类型、模块和所有东西的原因。因此,如果我正在编写一个负责处理此类输入的生产质量程序,那么它看起来会非常不同。

    当我用函数式语言编写程序时,我个人采用的一般风格是遵循以下两个简单规则:

    1. 如有疑问,请使用更多类型。
    2. 玩得开心(很多乐趣)。

    即为程序域中的每个概念分配一个类型,并使用许多小功能。

    下面的代码是原来的两倍,但更易读、更易维护、更健壮。

    所以,首先,让我们输入:条目只是一条记录。为简单起见,我使用字符串类型来表示电话。

    type entry = {
      name : string;
      phone : string;
    }
    

    任务中没有指定查询,所以我们只用一个字符串存根:

    type query = Q of string
    

    现在我们的解析器状态。我们有三种可能的状态:Start 状态,Entry n 状态,我们正在解析条目,到目前为止还剩下 n 条目,以及Query 状态,当我们解析查询时。

    type state =
      | Start
      | Entry of int
      | Query
    

    现在我们需要为每个状态编写一个函数,但首先,让我们定义一个错误处理策略。对于一个简单的程序,我建议只因解析器错误而失败。当我们的期望失败时,我们将调用一个名为 expect 的函数:

    let expect what got =
      failwithf "Parser error: expected %s got %s\n" what got ()
    

    现在三个解析函数:

    let parse_query s = Q s
    
    let parse_entry s line = match String.split ~on:' ' line with
      | [name;phone] ->  {name;phone}
      | _ -> expect "<name> <phone>" line
    
    let parse_expected s =
      try int_of_string s with exn ->
        expect "<number-of-entries>" s
    

    现在让我们编写解析器:

    let parse (es,qs,state) input = match state with
      | Start -> es,qs,Entry (parse_expected input)
      | Entry 0 -> es,qs,Query
      | Entry n -> parse_entry input :: es,qs,Entry (n-1)
      | Query -> es, parse_query input :: qs,Query
    

    最后,让我们从文件中读取数据:

    let of_file name =
      let es,qs,state =
        In_channel.with_file name ~f:(fun ch ->
          In_channel.fold_lines ch ~init:([],[],Start) ~f:parse) in
      match state with
      | Entry 0 | Query -> ()
      | Start -> expect "<number-of-entries><br>..." "<empty>"
      | Entry n -> expect (sprintf "%d entries" n) "fewer"
    

    我们还检查我们的状态机是否达到了正确的完成状态,即处于QueryEntry 0 状态。

    【讨论】:

      【解决方案2】:

      与 Python 一样,简洁实现的关键是让标准库完成大部分工作;以下代码使用 Sequence.fold 代替 Python 的列表理解。此外,使用Pervasives.input_line 而不是In_channel.input_line 可以减少无关的模式匹配(它会将文件结束条件报告为异常而不是None 结果)。

      open Core.Std
      
      module Dict = Map.Make(String)
      
      let n = int_of_string (input_line stdin)
      let d = Sequence.fold
        (Sequence.range 0 n)
        ~init:Dict.empty
        ~f:(fun d _ -> let line = input_line stdin in
            Scanf.sscanf line "%s %s" (fun k v -> Dict.add d ~key:k ~data:v))
      
      let () =
        try while true do
          let name = input_line stdin in
          match Dict.find d name with
          | Some number -> Printf.printf "%s=%s\n" name number
          | None -> Printf.printf "Not found.\n"
        done with End_of_file -> ()
      

      【讨论】:

      • 尼特,但我认为Sequence不在标准库中,而是在Core中。
      • OP 明确表示他想继续使用 Core,这就是我的代码基于此的原因(我通常不经常使用 Core)。
      • 是的,明白,不反对。我的意思是,在阅读第一句话时,我感觉到Sequence 在stdlib 中的含糊暗示,这可能会在以后使其他一些读者感到困惑。我意识到它并没有直接声称,它只是有点令人困惑。
      • 这可能只是一个术语。 Core、Batteries、ExtLib 和 Containers 通常都被描述为替代标准库,所以我倾向于使用 OCaml 这个术语。我现在意识到,如果您认为“标准库”指的是 OCaml 标准库,这可能会令人困惑。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-11-29
      • 2011-06-02
      • 2020-02-25
      相关资源
      最近更新 更多