Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

F# sequence with at least one element

Tags:

f#

Beginner in F# here

I want to create a type, which is a sequence of another concrete type (Event) with at least one element. Any other elements can be added anytime later. Normally in C# I would create a class with a private List<Event> and public methods.

But I want to do it with a functional approach and not imitate the C# approach. Or at least try.

My train of thought:

  • Let's create a type "of seq" and give it a constructor requiring instance of the Event type

    type Event = Event of string
    
    type PublishedEvents = EventList of seq<Event> with
        static member create (event:Event) = EventList(Seq.singleton event)
    
  • Now let's add an "add" method for adding another optional Event instances

    type PublishedEvents with
        member this.add(event:Event) = Seq.append this [event]
    

But that doesn't work, F# complains that "this" is not compatible with seq<'a>.

So I tried this:

type PublishedEvents with
    member this.add (event:Event) : PublishedEvents = EventList(Seq.append this [event])

Now it complains that "this" is not compatible with seq<Event>...which is confusing me now since few lines above it says EventList of seq<Event> ... so I guess I need to somehow convert EventList back to seq<Event> so I can then use Seq.append ?

let convertFunction (eventList:PublishedEvents) : seq<Event> = ???

But I have no idea how to do this.

Am I even going the right direction? Is it better for this to mimic a C# class with a backing field? Or am I missing something?

like image 220
Mirek Avatar asked Jan 03 '23 04:01

Mirek


2 Answers

The actual sequence of events is wrapped inside an EventList discriminated union case.

You can unwrap it and re-wrap it like this:

type PublishedEvents with
    member this.add(event:Event) =
        match this with
        | EventList events -> Seq.append events [event] |> EventList

However, I have to question the value of creating this PublishedEvents type in the first place, if it's just a single EventList case containing a sequence that requires you to wrap and unwrap values repeatedly.

Also, please be aware that this add method doesn't change the existing PublishedEvents. It creates a new one with a new sequence of events, because of the way that Seq.append works, because seq<'a> is actually just F#'s name for System.Collections.Generic.IEnumerable<'a>).

Furthermore, your approach does not prevent creation of a non-empty event sequence. EventList is a public constructor for PublishedEvents so you can just write:

EventList []

A simple way to make the type system enforce a non-empty sequence is this:

type NonEmptySeq<'a> = { Head : 'a; Tail : seq<'a> } with
    static member Create (x:'a) = { Head = x; Tail = [] }
    member this.Add x = { this with Tail = Seq.append this.Tail [x] }

let a = NonEmptySeq.Create (Event "A")
let b = a.Add (Event "B")

But again, these sequences are immutable. You could do something similar with a C# List<'a> if you need mutation. In F# it's called a ResizeArray<'a>:

type NonEmptyResizeArray<'a> = { Head : 'a; Tail : ResizeArray<'a> } with
    static member Create (x:'a) = { Head = x; Tail = ResizeArray [] }
    member this.Add x = this.Tail.Add x

let a = NonEmptyResizeArray.Create (Event "A")
a.Add (Event "B")
like image 73
TheQuickBrownFox Avatar answered Jan 09 '23 14:01

TheQuickBrownFox


I propose that you go even more functional and not create members for your types - have it done in your functions. For example this would achieve the same and I would argue it's more idiomatic F#:

type Event = Event of string
type PublishedEvents = EventList of Event * Event list

let create e = EventList (e,[])
let add (EventList(head,tail)) e = EventList(e,head::tail)
let convert (EventList(head,tail)) = head::tail |> Seq.ofList

let myNewList = create (Event "e1")
let myUpdatedList = add myNewList (Event "e2")
let sequence = convert myUpdatedList

val sequence : seq = [Event "e2"; Event "e1"]

On the other hand if your aim is to interop with C# your approach would be easier to consume on C# side.

like image 40
DevNewb Avatar answered Jan 09 '23 13:01

DevNewb