A C# code base translated to F#.

This article is part of a larger article series that examines variations of how to take on a non-trivial problem using functional architecture. In the previous article we established a baseline C# code base. Future articles are going to use that C# code base as a starting point for refactored code. On the other hand, I also want to demonstrate what such solutions may look like in languages like F# or Haskell. In this article, you'll see how to port the C# baseline to F#.

The code shown in this article is from the fsharp-port branch of the accompanying Git repository.

Data structures #

We may start by defining the required data structures. All are going to be records.

type User = { UserName : string; TotalScrobbleCount : int }

Just like the equivalent C# code, a User is just a string and an int.

When creating new values, record syntax can sometimes be awkward, so I also define a curried function to create User values:

let user userName totalScrobbleCount =
    { UserName = userName; TotalScrobbleCount = totalScrobbleCount }

Likewise, I define Song and Scrobble in the same way:

type Song = { Id : int; IsVerifiedArtist : bool; Rating : byte }
let song id isVerfiedArtist rating =
    { Id = id; IsVerifiedArtist = isVerfiedArtist; Rating = rating }
 
type Scrobble = { Song : Song; ScrobbleCount : int }
let scrobble song scrobbleCount = { Song = song; ScrobbleCount = scrobbleCount }

To be honest, I only use those curried functions sparingly, so they're somewhat redundant. Perhaps I should consider getting rid of them. For now, however, they stay.

Since I'm moving all the code to F#, I also have to translate the interface.

type SongService =
    abstract GetTopListenersAsync : songId : int -> Task<IReadOnlyCollection<User>>
    abstract GetTopScrobblesAsync : userName : string -> Task<IReadOnlyCollection<Scrobble>>

The syntax is different from C#, but otherwise, this is the same interface.

Implementation #

Those are all the supporting types required to implement the RecommendationsProvider. This is the most direct translation of the C# code that I could think of:

type RecommendationsProvider (songService : SongService) =
    member _.GetRecommendationsAsync userName = task {
        // 1. Get user's own top scrobbles
        // 2. Get other users who listened to the same songs
        // 3. Get top scrobbles of those users
        // 4. Aggregate the songs into recommendations
 
        // Impure
        let! scrobbles = songService.GetTopScrobblesAsync userName
 
        // Pure
        let scrobblesSnapshot =
            scrobbles
            |> Seq.sortByDescending (fun s -> s.ScrobbleCount)
            |> Seq.truncate 100
            |> Seq.toList
 
        let recommendationCandidates = ResizeArray ()
        for scrobble in scrobblesSnapshot do
            // Impure
            let! otherListeners =
                songService.GetTopListenersAsync scrobble.Song.Id
 
            // Pure
            let otherListenersSnapshot =
                otherListeners
                |> Seq.filter (fun u -> u.TotalScrobbleCount >= 10_000)
                |> Seq.sortByDescending (fun u -> u.TotalScrobbleCount)
                |> Seq.truncate 20
                |> Seq.toList
 
            for otherListener in otherListenersSnapshot do
                // Impure
                let! otherScrobbles =
                    songService.GetTopScrobblesAsync otherListener.UserName
 
                // Pure
                let otherScrobblesSnapshot =
                    otherScrobbles
                    |> Seq.filter (fun s -> s.Song.IsVerifiedArtist)
                    |> Seq.sortByDescending (fun s -> s.Song.Rating)
                    |> Seq.truncate 10
                    |> Seq.toList
 
                otherScrobblesSnapshot
                |> List.map (fun s -> s.Song)
                |> recommendationCandidates.AddRange
 
        // Pure
        let recommendations =
            recommendationCandidates
            |> Seq.sortByDescending (fun s -> s.Rating)
            |> Seq.truncate 200
            |> Seq.toList
            :> IReadOnlyCollection<_>
 
        return recommendations }

As you can tell, I've kept the comments from the original, too.

Test Double #

In the previous article, I'd written the Fake SongService in C#. Since, in this article, I'm translating everything to F#, I need to translate the Fake, too.

type FakeSongService () =
    let songs = ConcurrentDictionary<intSong> ()
    let users = ConcurrentDictionary<stringConcurrentDictionary<intint>> ()
 
    interface SongService with
        member _.GetTopListenersAsync songId =
            let listeners =
                users
                |> Seq.filter (fun kvp -> kvp.Value.ContainsKey songId)
                |> Seq.map (fun kvp -> user kvp.Key (Seq.sum kvp.Value.Values))
                |> Seq.toList
 
            Task.FromResult listeners
 
        member _.GetTopScrobblesAsync userName =
            let scrobbles =
                users.GetOrAdd(userNameConcurrentDictionary<_, _> ())
                |> Seq.map (fun kvp -> scrobble songs[kvp.Key] kvp.Value)
                |> Seq.toList
 
            Task.FromResult scrobbles
 
    member _.Scrobble (userNamesong : SongscrobbleCount) =
        let addScrobbles (scrobbles : ConcurrentDictionary<_, _>) =
            scrobbles.AddOrUpdate (
                song.Id,
                scrobbleCount,
                fun _ oldCount -> oldCount + scrobbleCount)
            |> ignore
            scrobbles
 
        users.AddOrUpdate (
            userName,
            ConcurrentDictionary<_, _>
                [ KeyValuePair.Create (song.Id, scrobbleCount) ],
            fun _ scrobbles -> addScrobbles scrobbles)
        |> ignore
        
        songs.AddOrUpdate (song.Id, songfun _ _ -> song) |> ignore

Apart from the code shown here, only minor changes were required for the tests, such as using those curried creation functions instead of constructors, a cast to SongService, and a few other non-behavioural things like that. All tests still pass, so I consider this a faithful translation of the C# code base.

Conclusion #

This article does more groundwork. Since it may be illuminating to see one problem represented in more than one programming language, I present it in both C#, F#, and Haskell. The next article does exactly that: Translates this F# code to Haskell. Once all three bases are established, we can start introducing solution variations.

If you don't care about the Haskell examples, you can always go back to the first article in this article series and use the table of contents to jump to the next C# example.

Next: Porting song recommendations to Haskell.



Wish to comment?

You can add a comment to this post by sending me a pull request. Alternatively, you can discuss this post on Twitter or somewhere else with a permalink. Ping me with the link, and I may respond.

Published

Monday, 14 April 2025 08:54:00 UTC

Tags



"Our team wholeheartedly endorses Mark. His expert service provides tremendous value."
Hire me!
Published: Monday, 14 April 2025 08:54:00 UTC