[SOLVED] How to cast one Record with optional fields to another?

Came up as part of this discussion, I’m trying to implement conversion of one Record to another without resorting to FFI. The gotcha here is that Records have all fields optional (which in PureScript is implemented via a Union).

It haven’t found anybody to ask this question, and when trying to implement this myself I get errors that make no sense to me, so just posting here, perhaps somebody knows what’s that about…

module Main where

import Prelude

import Effect (Effect)
import Prim.Row (class Union)

type Props_before = ( x :: Int,    y :: Int)
type Props_after  = ( x :: String, y :: String )

convertIntsToStrings :: ∀ lhs1 rhs1 lhs2 rhs2
                        . Union lhs1 rhs1 Props_before
                        => Union lhs2 rhs2 Props_after
                        => Record lhs1 -> Record lhs2
convertIntsToStrings p = { x : show p.x
                         , y : show p.y }

main :: Effect Unit
main = do
  let foo = {x: 1}
      ret = convertIntsToStrings foo
  pure unit

The error:

Error found:
in module Main
at src/Main.purs:15:37 - 15:38 (line 15, column 37 - line 15, column 38)

  Could not match type

    lhs12

  with type

    ( x :: t0
    | t1
    )


while checking that type Record lhs12
  is at least as general as type { x :: t0
                                 | t1
                                 }
while checking that expression p
  has type { x :: t0
           | t1
           }
while checking type of property accessor p.x
in value declaration convertIntsToStrings

where lhs12 is a rigid type variable
        bound at (line 0, column 0 - line 0, column 0)
      t0 is an unknown type
      t1 is an unknown type

I think your explanation and your code are quite misleading, it’s not clear what you are trying to do.

I am not sure which part is unclear to you, maybe you wanted to hear a usecase for Records with optional fields? It’s common for interaction with React. You can see example here, it’s a label element that accepts a Record with predefined optional fields.

Give a specific example of the records that you want to convert from to.

I’m not sure why you’d want a larger code instead of the minimal example, but sure, here. The props lack more fields which will be added later.

Code (click to unroll)
module Testing where

import Prelude

import Effect (Effect)
import Prim.Row (class Union)
import React.Basic.Hooks (JSX, ReactComponent)
import React.Basic.Hooks as React

data ButtonSize = ButtonSizeSM | ButtonSizeMD | ButtonSizeLG
instance Show ButtonSize where
  show ButtonSizeSM = "sm"
  show ButtonSizeMD = "md"
  show ButtonSizeLG = "lg"

type Props_button = (children :: Array JSX, size :: ButtonSize)
foreign import _button :: ∀ props. ReactComponent (Record props)

canonicalizeButtonSize :: ∀ lhs rhs props
                          . Union lhs rhs Props_button
                          => Record lhs -> Record props
canonicalizeButtonSize p = { size: show p.size, children: p.children }

button :: ∀ lhs rhs
   . Union lhs rhs Props_button
  => Record lhs -> JSX
button props = React.element _button (canonicalizeButtonSize props)

data CheckboxSize = CheckboxSizeSM | CheckboxSizeMD
instance Show CheckboxSize where
  show CheckboxSizeSM = "sm"
  show CheckboxSizeMD = "md"

type Props_checkbox = (children :: Array JSX, size :: CheckboxSize)
foreign import _checkbox :: ∀ props. ReactComponent (Record props)

canonicalizeCheckboxSize :: ∀ lhs rhs props
                          . Union lhs rhs Props_checkbox
                          => Record lhs -> Record props
canonicalizeCheckboxSize p = { size: show p.size, children: p.children }

checkbox :: ∀ lhs rhs
   . Union lhs rhs Props_checkbox
  => Record lhs -> JSX
checkbox props = React.element _checkbox (canonicalizeCheckboxSize props)

I am not 100% sure type-system-wise everything is correct, because this gives the exact same error as in the original post, and Idk how to get past it. But looks correct to me.

I asked not for the whole code but just two records: input and output.

What I poorly understand is why you are trying to do a simple task with such a complex approach, generic signatures, type classes stuff etc. Everything should be much simpler.

Why not do something like this:

(Disclaimer: I don’t use React in PS so this is kind of pseudo code anyway)

type RawCheckBoxProps = {size :: String, children :: Array JSX}

foreign import _checkbox :: ReactComponent RawCheckBoxProps

type CheckBoxProps = {size :: CheckboxSize, children :: Array JSX}

checkbox :: CheckBoxProps -> ReactComponent ...
checkbox {size, children} = _checkbox {size: toRawSize size, children }

Just use simple native types and plain functions to transform one thing into another.

If you want something more generic for props conversion it may be possible, but from your example it is not clear what you need and why.

Because what you’re suggesting doesn’t suit the usecase. Components have dozens of optional fields, which your approach forces to explicitly enlist. In your approach you can’t omit children if you only want size; ditto for other fields. The point of all this typeclass stuff is to make sure that if a user only wants to pass children and nothing else, they would just call button {children: [someJSX]} and it will work.

If you want such an API with optional properties it is probably better to use for example this: GitHub - natefaubion/purescript-convertable-options: Highly-overloaded APIs for PureScript

Or commonly it is accomplied with supplying function that transforms the default set of props.

But anyway, first of all I would suggest to think about how to make your API simpler and more explicit, specific for the use cases, then trying to make it generic and “flexible”.

Adding a 3rd-party library just for this is an overcomplication; and besides, do you know the library doesn’t have same problem? I don’t.

Over-complication is to make an overloaded generic API with optional fields for a specific application case.

You didn’t state the problem clearly.

Okay, listen, we have had so many discussions in so many different threads, and your replies often result in in too much discussion with very little insight to the solution. Maybe I’m bad somewhere in explanations, but please let’s limit this thread to solving the error for the title code. If you have example of using 3rd-party lib that would allow casting one Record with optional fields to another — by all means go for it. Thank you.

You didn’t state the problem clearly.

I literally provided you with minimal steps to reproduce, I can’t clarify it any further. If you consider this unclear, feel free to ignore this thread.

1 Like

Even disregarding the typing error, how do you want to pass a record {x: 1} that has only x property and access p.y inside the function? That is why I say your code is misleading, though you say it looks right to you.Your intentional inputs and outputs are poorly derivable from it.

My intention is written in the title, and reiterated in the first paragraph of thread description. I may not be using the correct syntax, but I don’t know what’s the correct one. But what I want is described by just one sentence: “convert one Record with optional fields to another”. The p.y somehow needs to be passed on in case it is provided, and the problem is we don’t know beforehand whether it will be provided, but in case it is that needs to be handled.

For this specific example, would the heterogeneous library accomplish what you’re after? purescript-heterogeneous - Pursuit

module Main where

import Prelude

import Effect (Effect)
import Effect.Class.Console as Console
import Heterogeneous.Mapping (class HMap, class Mapping, hmap)
import Prim.Row (class Union)

type Props_before = (x :: Int, y :: Int)
type Props_after = (x :: String, y :: String)

data ShowProps = ShowProps

instance
  ( Show props
  ) =>
  Mapping ShowProps props String where
  mapping ShowProps = show

convertIntsToStrings
  :: forall lhs1 rhs1 lhs2
   . Union lhs1 rhs1 Props_before
  => HMap ShowProps { | lhs1 } { | lhs2 }
  => Record lhs1
  -> Record lhs2
convertIntsToStrings p =
  hmap ShowProps p

main :: Effect Unit
main = do
  let
    foo = { x: 1 }
    ret = convertIntsToStrings foo
  Console.logShow ret -- { x: "1" }
1 Like

PureScript the language has no semantic concept of “records with optional fields” so you will always struggle to do this natively (ie. it’s largely impossible). If a record type doesn’t have a field, then it’s a different record type altogether. convertable-options exposes some utility typeclasses for converting between different record types. The other option is defining a different type altogether and working with that, such as with purescript-option.

2 Likes

Thank you both! The heterogeneous seems to be what I need, works for me, thank you!