6

因此,我制作了这个名为 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)我觉得可以使用列表表达式、maybe-expressions(来自 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值后停止映射。发生这种情况时,quitwill be true,其余元素将不会被映射。之后,如果quittrue,则丢弃部分映射的列表并None返回。如果它从不映射到None,它将最终构建一个用于构建映射列表的延续。

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

4

5 回答 5

2

我想出了一个使用Seq.scanand的实现Seq.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

状态不仅仅是当前值,而是包含先前值和当前值的一对。这样,我们可以很容易NoneSome使用takeWhile.

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

于 2014-04-16T04:02:42.493 回答
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当有 1 时,将函数应用于f选项内的值,否则返回. 这意味着我们得到了所需的短路:只要返回 None,就返回。xNonef xtryMap

如果我们导入maybeFSharpx 的 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' }
于 2014-04-16T08:34:39.170 回答
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但这读起来很好。

于 2014-04-16T14:34:40.050 回答
2

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

为了回答您的问题,以下是特定于列表(可遍历)和选项(应用程序)的实现。此处,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) 
于 2014-04-16T15:37:00.130 回答
1

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

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

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

你们怎么看这件事?:-)

于 2014-04-21T02:57:43.883 回答