F#:将 seq<'A> 转换为 seq<'B> 的最快方法

F#: Fastest way to convert a seq<'A> to seq<'B>

我将 Marten 用作事件存储,特别是用于获取事件流。

type AccountCreation = {
    Owner: string
    AccountId: Guid
    CreatedAt: DateTimeOffset
    StartingBalance: decimal
}

type AccountEvents =
    | AccountCreated of AccountCreation
    | AccountCredited of Transaction
    | AccountDebited of Transaction

let settings = {
    Host = "localhost"
    DatabaseName = "postgres"
    UserName = "root"
    Password = "root"
    EventTypes = eventTypes
}
use store = createDocumentStore settings
use session = store.LightweightSession()

let khalidId = Guid.NewGuid()
let billId = Guid.NewGuid()

let khalid = AccountEvents.AccountCreated({
    Owner = "Khalid Abuhakmeh"
    AccountId = khalidId
    StartingBalance = 1000m
    CreatedAt = DateTimeOffset.UtcNow
})

let bill = {
    Owner = "Bill Boga"
    AccountId = billId
    StartingBalance = 0m
    CreatedAt = DateTimeOffset.UtcNow
}

session.Events.Append(khalidId, khalid) |> ignore
session.Events.Append(billId, bill) |> ignore

session.SaveChanges()

let stream = session.Events.FetchStream()

streamIReadOnlyList<IEvent>IEvent 定义为:

public interface IEvent
{
    Guid Id { get; set; }
    int Version { get; set; }
    long Sequence { get; set; }
    object Data { get; }
    Guid StreamId { get; set; }
    string StreamKey { get; set; }
    DateTimeOffset Timestamp { get; set; }
    string TenantId { get; set; }
    void Apply<TAggregate>(TAggregate state, IAggregator<TAggregate> aggregator) where TAggregate : class, new();
}

我想将每个 IEvent 转换为 AccountEvents,如果 Data 属性 的基础类型是 AccountEvents(如果不是项目在结果序列中没有产生。

在 C# 中,我会简单地使用关键字 as 来实现它,但在 F# 中,我不确定最快的 F#-ish 方式(就性能而言)是什么。

我最终得到了以下代码:

let seqCastOption<'T> sequence =
    sequence
    |> Seq.map(fun x ->
        match box x with
        | :? 'T as value -> Some value
        | _ -> None)

let fetchStream<'T> (session: IDocumentSession) (id: Guid) =
    let stream = session.Events.FetchStream(id)
    stream
    |> Seq.map(fun x -> x.Data)
    |> seqCastOption<'T>
    |> Seq.filter (fun x -> x.IsSome)
    |> Seq.map(fun x -> x.Value)

但这看起来相当 "expensive",我想知道将 .Data 转换为 Option<AccountEvents> + 过滤 IsSome 的步骤是否可以全部完成立刻。

Seq.choose 是您一直在寻找的功能。你给它一个函数,它接受一个 'A 和 returns 一个 'B option,它产生 Some'B 值。对于您的使用场景,它看起来像这样:

let castOption<'T> x =
    match box x with
    | :? 'T as value -> Some value
    | _ -> None

let fetchStream<'T> (session: IDocumentSession) (id: Guid) =
    let stream = session.Events.FetchStream(id)
    stream
    |> Seq.map(fun x -> x.Data)
    |> Seq.choose castOption<'T>

rmunn 的回答中提到的 Seq.choose 函数对于了解这种情况非常有用,但对于这种确切情况,我建议使用内置的 .NET 方法 Enumerable.OfType<'T>,它确实正是您想要的,并且可能已经过优化:

open System.Linq

let fetchStream<'T> (session: IDocumentSession) (id: Guid) =
    let stream = session.Events.FetchStream(id)
    stream
    |> Seq.map(fun x -> x.Data)
    |> Enumerable.OfType<'T>