F# 如何避免两次强制转换

F# How to avoid twice cast

案例一

  1. 取元素 while is Some value from sequense
  2. 打印一些值
let a = seq { yield Some 1; yield Some 2; yield Some 3; yield None }

a
|> Seq.takeWhile Option.isSome // cast 1
|> Seq.map Option.get          // cast 2
|> Seq.iter (printfn "%A")

案例二

  1. 一些值的过滤序列
  2. 打印 Some 的值
a
|> Seq.filter Option.isSome    // cast 1
|> Seq.map Option.get          // cast 2
|> Seq.iter (printfn "%A")

案例三

  1. 按元素类型分组
  2. 打印每组的值
type AB =
    | A of a : int
    | B of b : string

let a = seq{
    yield A 1
    yield A 2
    yield B "ds"
    yield B "fsdf"
}

let (|As|Bs|) = function
    | A _ -> As
    | B _ -> Bs

let matcher = function
    | A a-> printfn "%A" a
    | B b -> printfn "%A" b

a
|> Seq.groupBy (|As|Bs|)       // cast 1
|> Seq.map snd
|> Seq.iter (Seq.iter matcher) // cast 2

为什么我需要避免双重施法?

对于 "Case 2" 你可以使用 Seq.choose 和恒等函数,id:

a
|> Seq.choose id
|> Seq.iter (printfn "%A")

Seq.choosedocumentation

Applies the given function to each element of the list and returns the list comprised of the results for each element where the function returns Some with some value.

传递给它的恒等函数将因此 return 每个 Option 值的内容,即 Some.

如果您正在使用列表, 您可以做与 Haskell 中相同的事情(根据您的评论判断,您已经知道)。 F# 也有模式匹配,它的工作方式大致相同,除了语言不是懒惰的,所以你必须考虑到这一点。例如:

let rec case2 xs = 
    match xs with
    | (Some x)::rest -> printfn "%A" x; case2 rest
    | None::rest -> case2 rest
    | [] -> ()

(注意:如果您正在执行显式递归,最好将其保持在 "tail" 范围内;在 Haskell 中,由于懒惰,这并不重要,但在.NET 如果你不小心,你很容易炸毁堆栈)


如果您使用的是序列,情况会稍微复杂一些。对于某些事情,您可以使用计算表达式(它们与 Haskell 中的 do 符号有些相关):

let case2 xs = seq {
    for x in xs do
        match x with | Some a -> yield a | None -> ()
}

或者在某些情况下,标准库函数:

let case2 xs = Seq.choose id xs

(注:以上例子由于数值限制不能缩减eta)

但是您的第一个示例 ("early stop") 不能像那样以声明方式表达。您仍然必须使用 takeWhile,但至少您可以使用 choose 而不是 map 来避免使用偏函数:

let case1 xs = xs |> Seq.takeWhile Option.isSome |> Seq.choose id

如果您真的-真的想要一个模式匹配而不是两个,可以更深入一层并直接使用 IEnumerable 接口:

let case1 (xs: seq<_>) = seq {
  use e = xs.GetEnumerator()
  let mutable stop = false

  while not stop && e.MoveNext() do
    match e.Current with
    | Some x -> yield x
    | None -> stop <- true
}

请注意,这使用了可变变量,虽然乍一看很难看,但值得记住的是,遍历 .NET 序列(又名 IEnumerable<'t>)本质上是一个基于突变的过程。看上面:e.MoveNext() 调用改变了枚举器 e 的状态。如果你想下降到这个水平,你必须处理这个事实。

当然,我可以通过用递归替换它来消除 stop 变量:

let case1 (xs: seq<_>) = seq {
  use e = xs.GetEnumerator()

  let rec loop() = seq {
    if e.MoveNext() then
      match e.Current with
      | Some x -> yield x; yield! loop()
      | None -> ()
  }

  yield! loop()
}

但这很愚蠢:如果我愿意处理可变枚举器,我还不如全力以赴。


至于你的第三个例子 - 我什至不明白你想在那里做什么。或者,更确切地说,我确实理解它的作用,但我不明白摆脱第二场比赛意味着什么。也许您可以在 Haskell?

中说明如何做同样的事情