使用部分函数短路列表映射

时间:2014-04-16 03:31:12

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

所以,我把这个函数叫做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

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

以下是如何使用它的示例:

鉴于此功能......

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'maybe-expressions。

更新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,它将最终构建一个构建映射列表的延续。

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

5 个答案:

答案 0 :(得分:2)

我想出了一个使用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

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

编辑:嗯,当我第一次写它时它更短,但是通过评论和更好的格式化,它可能与你原来的功能一样长。所以也许这不是一个改进: - )

答案 1 :(得分:2)

以下是monadic / workflow风格的变体:

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返回None,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' }

答案 2 :(得分:2)

这是一种直接的方法:

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保存一些字符,但这很好看。

答案 3 :(得分:2)

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

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

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) 

答案 4 :(得分:1)

根据Daniel和SørenDebois的回答和评论,我想出了以下内容:

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

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

你们怎么看待这个? : - )