F# sequence with at least one element
F# sequence with at least one element
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
?
EventList of seq<Event>
EventList
seq<Event>
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?
2 Answers
2
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.
The actual sequence of events is wrapped inside an EventList
discriminated union case.
EventList
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.
PublishedEvents
EventList
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>)
.
add
PublishedEvents
Seq.append
seq<'a>
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
PublishedEvents
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>
:
List<'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")
By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.