3

I'm very new to F# and trying to figure out the best way to convert between similar but different data objects while minimising duplication of code. I'm writing an app using the Fabulous F# MVU framework for Xamarin Forms.

I'm using sqlite-net-pcl for data persistence, which calls for an object when creating a database table and interacting with that table, and I have created an AuthorObject:

    type AuthorObject() =
        [<PrimaryKey>] // sqlite-net-pcl attribute
        member val AuthorId = 0 with get, set
        member val Username = "" with get, set
        member val Token = "" with get, set

Fabulous makes use of record types for models and I have created an Author record type to pass around inside Fabulous:

    type Author = 
        { AuthorId: int
          Username: string
          Token: string }

But I also have to interact with a web service API library (written in C#) that returns an AuthorDetailDto object. AuthorDetailDto has the same named properties on it as above, plus more that I don't need to worry about. I cannot change the definition of AuthorDetailDto.

So it seems the constraints are currently that sqlite-net-pcl needs an object with members, Fabulous needs a record type, and I have this AuthorDetailDto object coming across from the service API client library that I can't change. I need to convert between these types and I initially was hoping that I could get away with just:

    let convertToObject author = // Was hoping author param here could be generic in some way
        let obj = AuthorObject()
        obj.AuthorId <- author.AuthorId
        obj.Username <- author.Username
        obj.Token <- author.Token
        obj

    let convertToModel (obj: AuthorObject) : Author =
        { AuthorId = obj.AuthorId
          Username = obj.Username
          Token = obj.Token }

But with the added complication of AuthorDetailDto, I'm finding I now need to do something like this to keep the compiler happy:

    let convertAuthorDetailDtoToObject (dto: AuthorDetailDto) =
        let obj = AuthorObject()
        obj.AuthorId <- dto.AuthorId
        obj.Username <- dto.Username
        obj.Token <- dto.Token
        obj

    let convertAuthorToObject (author: Author) =
        let obj = AuthorObject()
        obj.AuthorId <- author.AuthorId
        obj.Username <- author.Username
        obj.Token <- author.Token
        obj

    let convertToModel (obj: AuthorObject) : Author =
        { AuthorId = obj.AuthorId
          Username = obj.Username
          Token = obj.Token }

Here are some source files:

DomainModels.fs:

namespace MyApp.Mobile

module DomainModels =
    type Author = 
        { AuthorId: int
          Username: string
          Token: string }

Author.fs:

namespace MyApp.Mobile.Repository

open SQLite
open Client // Provides AuthorDetailDto.
open MyApp.Mobile.DomainModels

module Author =
    type AuthorObject() =
        [<PrimaryKey>]
        member val AuthorId = 0 with get, set
        member val Username = "" with get, set
        member val Token = "" with get, set

    let convertToObject author =
        let obj = AuthorObject()
        obj.AuthorId <- author.AuthorId
        obj.Username <- author.Username
        obj.Token <- author.Token
        obj

    let convertToModel (obj: AuthorObject) : Author =
        { AuthorId = obj.AuthorId
          Username = obj.Username
          Token = obj.Token }

    let connect dbPath = async {
            let db = SQLiteAsyncConnection(SQLiteConnectionString dbPath)
            do! db.CreateTableAsync<AuthorObject>() |> Async.AwaitTask |> Async.Ignore
            return db
    }

    let purgeLoggedInAuthor dbPath = async {
        let! db = connect dbPath
        do! db.ExecuteAsync "DELETE FROM AuthorObject" |> Async.AwaitTask |> Async.Ignore
    }

    let insertLoggedInAuthor dbPath author = async {
        let! db = connect dbPath
        do! db.InsertAsync (convertToObject author) |> Async.AwaitTask |> Async.Ignore
    }

The initial call looks like this:

insertLoggedInAuthor dbPath loggedInAuthor // loggedInAuthor is an AuthorDetailDto.

The error is FS0001 This expression was expected to have type 'DomainModels.Author' but here has type 'AuthorDetailDto' due to both types being referenced in Author.fs.

I've tried messing around with inline and static member constraints, and attempting to define an interface, but due to Author needing to be a record type and not being able to change AuthorDetailDto, it doesn't seem like this approach is viable. Or perhaps I'm just not well versed enough in F#.

To those more familiar with F# than me, what is the best way to address this kind of situation to minimise code duplication?

GregC
  • 7,737
  • 2
  • 53
  • 67
gaelian
  • 105
  • 10
  • I'm not familiar with the tools you're using, but I just want to point out that the only significant difference between "an object with members" and a record type is that record fields are immutable by default. Perhaps you could make your record type's fields mutable in order to satisfy both sqlite-net-pcl and Fabulous? – Brian Berns Jan 26 '21 at 01:16
  • @brianberns - yep thanks, this has occurred to me. I'm not sure if using an object rather than a record type will run me afoul of the Fabulous framework in some way that I'm not aware of, as the examples of Fabulous apps I'm working off have both "an object with members" for the SQLite class and a separate record type for the Fabulous model. So I took this to mean there was some good reason for that. But it's worth a try to find out. – gaelian Jan 26 '21 at 04:54
  • Perhaps you can try to create an Author record with mutable fields: `type Author = { mutable AuthorId: int; mutable Username: string; mutable Token: string } ` The resulting class has getters and setters. [Sharplab link](https://sharplab.io/#v2:DYLgZgzgPgBALgTwA4FMYEECucAWB7AJxgF4YBYAKBmpuoG8YBbbAQwCNg0tdCBJAExAwAlgDs4lWlKasOaAKoQUBUS0YohEOATEBzSdJrM47TjAAqeANYpRm7XpgBfIA===) – Piotr Rodak Jan 26 '21 at 14:25
  • `the examples of Fabulous apps I'm working off` - do you care to list some of these examples? I'm in a similar position of trying to use Fabulous with SQLite. Maybe I can help share my thoughts after I've messed with it some, though I realize you may already have figured this out by now. – Bondolin Mar 08 '22 at 02:51
  • 1
    @Bondolin for SQLite specifically, see and https://github.com/TimLariviere/FabulousContacts. Noting that my use of SQLite is by now more involved than what's in that example app. I ended up accepting some degree of partial duplication across the different kinds of objects I describe in my post, as there were enough differences and corner cases that it ended up mostly making sense to do so. And I wanted to retain the immutable record types for use in the app above the database layer. – gaelian Mar 08 '22 at 05:07

0 Answers0