【问题标题】:Short circuiting a list mapping with a partial function使用部分函数短路列表映射
【发布时间】:2014-05-30 16:17:56
【问题描述】:

所以,我制作了这个名为 tryMap 的函数,如下所示:

/// tryMap, with failure and success continuations.
let rec tryMapC : 'R -> ('U list -> 'R) -> ('T -> 'U option) -> ('T list) -> 'R =
    fun failure success mapping list -> 
        match list with
        | []         -> success []
        | head::tail -> 
            match mapping head with
            | None        -> failure
            | Some result -> 
                let success = (fun results -> result::results |> success)
                tryMapC failure success mapping tail

/// <summary>
/// Attempts to map a list with a partial function.
/// <para/>
/// If a value which maps to None is encountered, 
/// the mapping stops, and returns None.
/// <para/>
/// Else, Some(list), containing the mapped values, is returned.
/// </summary>
let tryMap : ('T -> 'U option) -> 'T list -> 'U list option = 
    fun mapping list -> 
        tryMapC None Some mapping list

如其文档中所述,其目的是使用部分函数映射列表,如果“完整”映射不是“可能”,则使用简短的 curcuit,因为缺少更好的词。

这是一个如何使用它的示例:

鉴于此功能...

let tryFac n = 
    do printf "The factorial of %d" n
    if n < 0 then 
        do printf " cannot be computed.\n"
        None
    else
        let result = (List.fold (*) 1 [1..n])
        do printf " is %d\n" result
        Some result

...我们现在可以使用 tryMap 对整数列表进行全有或全无映射,如下所示:

> let all = tryMap tryFac [1..5];;
The factorial of 1 is 1
The factorial of 2 is 2
The factorial of 3 is 6
The factorial of 4 is 24
The factorial of 5 is 120
val all : int list option = Some [1; 2; 6; 24; 120]

> let nothing = tryMap tryFac [1;2;-3;4;5];;
The factorial of 1 is 1
The factorial of 2 is 2
The factorial of -3 cannot be computed.
val nothing : int list option = None

然后,例如,计算这些值的总和将很容易 - 如果它们可以全部被计算出来,那就是。

现在,我的问题是:

有没有更简单/更简洁的方法来实现这个 tryMap 函数? (当然,除了不那么冗长。:-P) 我有一种感觉,可以使用列表表达式、可能表达式(来自 FSharpx)或两者的组合来完成一些聪明的事情,但目前我不太清楚如何。 :-/

PS:如果你有一个比“tryMap”更好的名字,不要犹豫,留下评论。 :-)

更新 1:

我想出了这个版本,它与我的想法非常接近,但遗憾的是它没有短路。 :-/

let tryMap : ('T -> 'U option) -> 'T list -> 'U list option = 
    fun mapping list -> maybe { for value in list do return mapping value }

注意:这使用了 FSharpx 的可能表达式。

更新 2:

感谢Tomas Petricek,我想到了一个替代方案:

let tryMap (mapping : 'T -> 'U option) (list : 'T list) : 'U list option =
    List.fold
        (
            fun (cont,quit) value -> 
                if quit then 
                    (cont,quit)
                else
                    match mapping value with
                    | None   -> (cont,true)
                    | Some r -> ((fun rs -> (r::rs)) >> cont,quit)
        )
        (id,false)
        list
    |> (fun (cont,quit) -> if quit then None else Some (cont []))

此函数在映射到其第一个 None 值后停止映射。发生这种情况时,quit 将是 true,其余元素将不会被映射。之后,如果quittrue,则丢弃部分映射的列表并返回None。如果它从不映射到None,它将最终构建一个用于构建映射列表的延续。

虽然它仍然很大,现在它只是做了一个“轻微”的短路,因为它停止尝试映射列表,但它仍然遍历它,因为这就是折叠的工作方式。 :-/

【问题讨论】:

  • 看来你重新发明了mapM / traverse。 (两者都在FSharpPlus 中以更通用的方式定义)
  • 嗯。有趣的。我确实认为这种模式似乎很熟悉。 :-) 你能举例说明如何使用 mapM / traverse 来“解决问题”吗?
  • @Phazyck 看看我的回答

标签: list f# mapping short-circuiting partial-functions


【解决方案1】:

我想出了一个使用Seq.scanSeq.takeWhile 的实现,它更短,但不是特别优雅。它使用 Seq 是因为它的惰性 - 这样一来,我们就不会为不需要的值运行计算,因为一些早期的函数已经失败。

这个想法是产生一系列中间状态 - 所以我们将从 Some [] 开始,然后是
Some [first]Some [second; first] 等等,最后可能是 None功能失败。它将新值附加到前面 - 这很好,因为我们以后可以轻松地反转列表。使用tryFac作为调用函数,如下所示:

[1;2;-3;4;5] 
|> Seq.scan (fun (prev, st) v ->
  // If we failed when calculating previous function, return `None`
  match st with
  | None -> st, None
  | Some l ->
    // Otherwise run the function and see if it worked
    match tryFac v with
    | Some n -> st, Some (n::l) // If so, add new value
    | _ ->      st, None )      // Otherwise, return None
    (Some[], Some[]) // Using empty list as the initial state
  // Take while the previous value was 'Some'
  |> Seq.takeWhile (function Some _, _ -> true | _ -> false)
  // Take the last value we got and reverse the list
  |> Seq.last |> snd |> Option.map List.rev

状态不仅仅是当前值,而是包含先前值和当前值的一对。这样,我们可以使用takeWhile轻松获取第一个None或最后一个Some值。

编辑:好吧,我第一次写它的时候它比较短,但是有了 cmets 和更好的格式,它可能和你原来的函数一样长。所以也许这并不是真正的改进:-)

【讨论】:

    【解决方案2】:

    这里是一元/工作流风格的变体:

    let rec tryMap f = function 
        | [] -> Some [] 
        | x :: xs -> f x         |> Option.bind (fun x' -> 
                     tryMap f xs |> Option.bind (fun xs' -> 
                     x' :: xs'   |> Some))
    

    Option.bind f x 将函数f 应用于选项x 中的值,如果有,则返回None。这意味着我们得到了所需的短路:只要f x 返回无,tryMap 就会返回。

    如果我们导入 FSharpx 的 maybe monad,我们就可以使用工作流语法:

    let maybe = FSharpx.Option.maybe
    let rec tryMap2 f = function 
        | [] -> maybe { return [] }
        | x :: xs -> maybe { let! x' = f x
                             let! xs' = tryMap2 f xs 
                             return x' :: xs' }
    

    【讨论】:

    • 这看起来很不错!是否也可以使其尾递归? :-)
    • 当然,但你得到的只是@Daniel 使用Option.bind 而不是他嵌套的match f x with ... 的解决方案。
    【解决方案3】:

    正如 Mauricio Scheffer 所指出的,这是更一般的遍历操作的特定实例。如果您对某些背景感兴趣,遍历操作是在可遍历的结构(例如列表)上定义的,并支持映射到应用结构,其中选项是一个实例。如您所见,折叠列表和遍历列表之间存在相似之处。相似性是由于 monoid 和 applicative 之间的关系。

    为了回答您的问题,以下是特定于列表(可遍历)和选项(应用程序)的实现。此处,traverse 是通过事后映射按顺序定义的。这简化了实现。

    module List =
    
        let rec sequenceO (ls:list<'a option>) : list<'a> option =
            match ls with
            | [] -> Some []
            | x::xs -> 
                match x with
                | Some x -> sequenceO xs |> Option.map (fun ls -> x::ls)
                | None -> None
    
        let traverseO (f:'a -> 'b option) (ls:list<'a>) : list<'b> option =
            sequenceO (List.map f ls) 
    

    【讨论】:

    • 有趣的东西。 :-) 不过,您提供的实现不会短路,是吗?
    • 确实如此。尝试通过在 sequenceO 函数顶部放置 printfn 并运行 [ Some 1 ; None ; Some 2 ]
    • 哦,对了。但是 traverseO 不会使映射短路。 :-)
    • 真的!一个基于 Seq 的 impl 会。
    • 是的,这种差异似乎来自懒惰与严格的评估。
    【解决方案4】:

    这是一种简单的方法:

    let tryMap f xs =
        let rec loop ys = function
            | [] -> Some (List.rev ys)
            | x::xs ->
                match f x with
                | None -> None
                | Some y -> loop (y::ys) xs
        loop [] xs
    

    您可以使用Option.bind 保存一些字符,但这很好读。

    【讨论】:

    • 这看起来很整洁。不过,我有点想避免在之后反转列表 - 这就是我的第一个实现使用延续的原因。 :-)
    • 为什么要避免反转列表?如果您担心性能,请参阅this answer,它针对延续进行了基准测试。
    【解决方案5】:

    根据 Daniel 和 Søren Debois 的答案和 cmets,我得出以下结论:

    let tryMap f xs =
        let rec loop ys = function
            | []    -> maybe { return List.rev ys }
            | x::xs -> maybe { let! y = f x in return! loop (y::ys) xs }
        loop [] xs
    

    我不太确定它是否是尾递归的,因为我不是计算内部工作的专家 - (或者在这种情况下,具体来说:也许 - )表达式。

    你们怎么看这个? :-)

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-11-18
      • 2018-04-03
      • 1970-01-01
      • 1970-01-01
      • 2022-10-02
      相关资源
      最近更新 更多