Natalie Perret
Natalie Perret

Reputation: 8997

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

I am using Marten as an Event Store, in particular to fetch a stream of events.

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()

stream being IReadOnlyList<IEvent> and IEvent defined as:

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();
}

I would like to convert each IEvent to AccountEvents, if the underlying type of the Data property is AccountEvents (if not the item is not yielded in the resulting sequence).

In C# I would simply use the keyword as to achieve that, but in F# I am not sure what is the fastest F#-ish way(in terms of performance) to get that.

I ended up on the following code:

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)

But this seems quite "expensive", and I an wondering whether the step of converting .Data to the Option<AccountEvents> + filter the ones that IsSome can be done all at once.

Upvotes: 2

Views: 108

Answers (2)

TheQuickBrownFox
TheQuickBrownFox

Reputation: 10624

The Seq.choose function mentioned in rmunn's answer is very useful to know for this kind of situation, but for this exact situation I would recommend using the built in .NET method Enumerable.OfType<'T>, which does exactly what you want and is probably quite optimised:

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>

Upvotes: 6

rmunn
rmunn

Reputation: 36708

Seq.choose is the function you've been looking for. You give it a function that takes an 'A and returns a 'B option, and it yields the 'B value of the ones that were Some. For your usage scenario, it would look like this:

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>

Upvotes: 3

Related Questions