amitaibu
amitaibu

Reputation: 1116

Get a record outside of an `Include'`

I have a post that has an Include' as I've fetched it with a fetchRelated, so it has this type:

post :: Include' ["comments"] Post

If I'd like to save it, I'd get an error

post 
  |> set #title "Foo"
  |> updateRecord

The compiler will complain it's not a Post. It's an Include ... Post

My question is - can the Post be extracted out of the include, so I won't have to fetch it again from the DB?

Upvotes: 4

Views: 98

Answers (2)

Chris Schankula
Chris Schankula

Reputation: 347

I wanted to have a little fun and see if I could generalize Marc's answer above. Here's what I came up with! This is for a single include:

clearInclude :: forall model field fieldValue. (
        UpdateField field (Include field model) model fieldValue fieldValue,
        HasField field model fieldValue,
        Record model
    ) => Include field model -> model
clearInclude model = 
    model 
        |> updateField @field (getField @field (newRecord @model))

This should work with any datatype with a single Included data type. Then I tried it with two and three, which is pretty easy:

clearInclude2 :: _ => Include field1 (Include field2 model) -> model
clearInclude2 model =
    model 
        |> clearInclude
        |> clearInclude

clearInclude3 :: _ => Include field1 (Include field2 (Include field3 model)) -> model
clearInclude3 model =
    model 
        |> clearInclude
        |> clearInclude
        |> clearInclude

I still haven't figured out how to get it to compile when I fill out the typeclass constraints fully, and I still need to figure out how to make it work with the Include' compact notation, but this was a fun problem to get started on, and maybe we can continue to generalize further :)

Upvotes: 1

Marc Scholten
Marc Scholten

Reputation: 1396

There's no built-in function in IHP to do this. But you can use a custom helper like this:

clearComments :: Include "comments" Post -> Post
clearComments post = updateField @"comments" (newRecord @Post).comments post

And then use it like this:

post 
  |> set #title "Foo"
  |> clearComments
  |> updateRecord

Things can get slightly more complex when we have multiple Include. The compiler will error out,

clearIncludes :: Include ["comments", "tags"] Post -> Post
clearIncludes post = post
              |> updateField @"comments" (newRecord @Post).comments
              |> updateField @"tags" (newRecord @Post).tags

We need to split this into multiple functions, each with types annotations:

clear1 :: Include' ["comments", "tags"] Post -> Include "tags" Post
clear1 post = post
       |> updateField @"comments" (newRecord @Post).comments


clear2 :: Include "tags" Post -> Post
clear2 post = post
       |> updateField @"tags" (newRecord @Post).tags

March has explained the reason:

TL;DR: updateField has a more open type signature than set and sometimes GHC needs a bit of help

In very early IHP versions updateField was actually set and it was later changed (because it was causing errors like these). The core problem with updateField is, that it's type definition is very open. It's defined as updateField :: value' -> model -> model'. Here model and model' are two independent type variables. This means a call to updateField can actually change the return type of the record (e.g. turning Post to Post "tags" LandingPage). The problem with your first version was that GHC could not figure out the model' variable because multiple updateField were chained (the model type argument is easy for GHC to figure out, it's just that the model' is independent of that).

For comparison set is defined as set :: value -> model -> model. In that case when GHC can figure out model it will never error. So set is typically easy to figure out and unlikely to error. One the other side the problem with set is that it's less flexible and cannot change the output type (e.g. with model = Post it will be set :: value -> Post -> Post, so there's no way to express e.g. the Include stuff). That's why updateField exists.

Upvotes: 4

Related Questions