【问题标题】:How to reduce code clutter in this function?如何减少此函数中的代码混乱?
【发布时间】:2014-11-10 19:38:08
【问题描述】:

下面的函数tally 非常简单:它以字符串s 作为参数,将其拆分为非字母数字字符,并计算结果“单词”的数量,不区分大小写。

open Core.Std

let tally s =
  let get m k =
    match Map.find m k with
    | None   -> 0
    | Some n -> n
  in

  let upd m k = Map.add m ~key:k ~data:(1 + get m k) in

  let re = Str.regexp "[^a-zA-Z0-9]+" in
  let ws = List.map (Str.split re s) ~f:String.lowercase in

  List.fold_left ws ~init:String.Map.empty ~f:upd

我认为由于混乱,这个函数比它应该的更难阅读。我希望我能写一些更接近这个的东西(我沉迷于一些“幻想语法”):

(* NOT VALID SYNTAX -- DO NOT COPY !!! *)

open Core.Std

let tally s =

  let get m k =
        match find m k with
        | None   -> 0
        | Some n -> n ,

      upd m k = add m k (1 + get m k) ,

      re = regexp "[^a-zA-Z0-9]+" ,
      ws = map (split re s) lowercase 

  in fold_left ws empty upd

我在上面所做的更改主要分为三组:

  1. 摆脱重复的let ... in,合并所有绑定(到,-分隔的序列中;这,AFAIK,不是有效的OCaml);
  2. 消除了函数调用中的~foo:-type 噪音;
  3. 去掉了Str.List.等前缀。

我可以使用有效的 OCaml 语法实现类似的效果吗?

【问题讨论】:

  • fyi,Map.change 是更新地图条目的更自然选择:let update m k = String.Map.change m k (function None -> Some 1 | Some n -> Some (n+1))
  • @ivg:谢谢!顺便说一句,为什么Some 1 | ... Some (n+1),而不仅仅是1 | ... (n+1)?我以前见过这样的函数(即返回 only Some's),但我看不出它们的基本原理。
  • change key f 是一个瑞士刀函数。它的语义如下:如果key不存在,则fNone调用,否则用Some v调用,其中v是绑定到key的值。如果函数f返回Some u,则key会反弹到u,如果返回None,则key将被移除(如果存在)。
  • @ivg:好的,知道了。谢谢!

标签: ocaml


【解决方案1】:

可读性很难做到,很大程度上取决于读者的能力和对代码的熟悉程度。我将只关注语法转换,但如果这是您真正想要的,您也许可以将代码重构为更紧凑的形式。

要删除模块限定符,只需事先打开它们:

open Str 
open Map
open List

您必须按此顺序打开它们,以确保您在那里使用的 List 值仍然可以访问,并且不会被 Map 值覆盖。

对于带标签的参数,如果您为每个函数调用按函数签名顺序提供函数的所有参数,则可以省略标签。

要减少 let...in 构造的数量,您有多种选择:

  1. 使用一组rec定义:

    let tally s =
      let rec get m k =
         match find m k with
         | None   -> 0
         | Some n -> n
    
      and upd m k = add  m k (1 + get m k)
    
      and re = regexp "[^a-zA-Z0-9]+" 
      and ws = map lowercase (split re s)
    
    in fold_left ws empty upd
    
  2. 一次定义多个:

    let tally s =
      let get, upd, ws =
        let  re = regexp "[^a-zA-Z0-9]+"  in
        fun m k ->
         match find m k with
         | None   -> 0
         | Some n -> n,
      fun g m k -> add  m k (1 + g m k),
      map lowercase (split re s)
    
    in fold_left ws empty (upd get)
    
  3. 使用模块对定义进行分组:

    let tally s =
      let module M = struct 
        let get m k =
          match find m k with
          | None   -> 0
          | Some n -> n
    
        let upd m k = add m k (1 + get m k)
    
        let re = regexp "[^a-zA-Z0-9]+" 
        let ws = map lowercase (split re s)
    
    end in fold_left ws empty M.upd
    

后者让人想起 Sml 语法,也许更适合编译器进行适当优化,但它只是去掉了 in 关键字。

请注意,由于我不熟悉 Core Api,我可能编写了错误的代码。

【讨论】:

  • 对于第一个选项,您确实不需要使绑定递归,即let x = 2 and y = 3 仍然有效,并且更干净。
  • @ivg 实际上你说得对:我可以像在第二个选项中那样解开定义,并完全删除 rec 绑定!不过,我仍然有 2 个 let 范围,一个用于正则表达式,一个用于其余范围。
【解决方案2】:

如果你对同一个值进行一系列计算,那么在 OCaml 中有一个 |> 运算符,它从左侧获取一个值,并应用于右侧的函数。这可以帮助您“摆脱”letin。关于带标签的参数,您可以通过回退到普通标准库来摆脱它们,并使您的代码更小,但可读性更低。无论如何,有一小块带有标签参数的糖,你总是可以写f ~key ~data而不是f ~key:key ~data:data。最后,可以通过本地开放语法 (let open List in ...) 或通过将其本地缩短为更小的名称 (let module L = List in) 来删除模块名称。

无论如何,我想向您展示一个我认为包含较少混乱的代码:

open Core.Std
open Re2.Std
open Re2.Infix
module Words = String.Map

let tally s =
  Re2.split ~/"\\PL" s |>
  List.map ~f:(fun s -> String.uppercase s, ()) |>
  Words.of_alist_multi |>
  Words.map ~f:List.length

【讨论】:

  • fwiw,当我第一次开始使用 ocaml 时,我也对 let..in 的链条感到恼火。这似乎是常见的成语。我现在在心理上将其过滤掉。 (|>) 管道样式为您节省了 let..in,但通常它需要显式使用标记的参数以使它们以正确的顺序排列 (|>)。
猜你喜欢
  • 1970-01-01
  • 2021-03-30
  • 1970-01-01
  • 2012-03-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-12-18
  • 1970-01-01
相关资源
最近更新 更多