Skip to content

Instantly share code, notes, and snippets.

@oldfartdeveloper
Last active November 30, 2021 16:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save oldfartdeveloper/6ed1454c82494015f8a22624366839bd to your computer and use it in GitHub Desktop.
Save oldfartdeveloper/6ed1454c82494015f8a22624366839bd to your computer and use it in GitHub Desktop.
PureScript: using newtype with records

PureScript: using newtype with records

I ran into a beginner mistake that I was unable to figure out. So I posted to PureScript Discord and the conversation quickly showed me not only why what I did didn't work, but suggestions for how to fix it specifically. Kudos to @monoidmusician and @natefabion. Here's the discussion:

me

I have a compiler error that I'm just not understanding why it's happening: Here's the test code:

module Foo where

import Data.Generic.Rep (class Generic)
import Data.List (List(..), filter, length, null)
import Data.List.Types ((:))
import Data.Show (class Show)
import Data.Show.Generic (genericShow)

newtype Foo = Foo { bars :: List Bar }

derive instance Generic Foo _
instance Show Foo where
  show = genericShow

newtype Bar = Bar { bar :: Int }

derive instance Generic Bar _
instance Show Bar where
  show = genericShow

In repl, I populate instances of Foo and Bar:

> import Foo           
> bar = Bar { bar : 1 }
> foo = Foo { bars : ( bar : Nil ) }
> foo
(Foo { bars: ((Bar { bar: 1 }) : Nil) })

So far, so good. I'm seeing what I expect.

But, when I attempt to access bars from foo, I get the TypesDoNotUnify error as follows:

> foo.bars
Error found:
in module $PSCI
at :1:1 - 1:4 (line 1, column 1 - line 1, column 4)

  Could not match type
                
    { bars :: t0
    | t1        
    }           
                
  with type
       
    Foo
       

while checking that type Foo
  is at least as general as type { bars :: t0
                                 | t1        
                                 }           
while checking that expression foo
  has type { bars :: t0
           | t1        
           }           
while checking type of property accessor foo.bars
in value declaration it

where t0 is an unknown type
      t1 is an unknown type

See https://github.com/purescript/documentation/blob/master/errors/TypesDoNotUnify.md for more information,
or to contribute content related to this error.


>

Why does foo.bars not work?

natefaubion

A newtype is an opaque nominal type. It's semantically equivalent to a data declaration. With data/newtype you are declaring a constructor, and to access the value you must pattern match on it. Foo is not a record, and so doesn't support dot syntax. It's a constructor around a record, and you need to unwrap it first before you can invoke .bars. Dot syntax only works on things of type Record ... or { ... } (which is sugar for Record).

me

@natefaubion Appreciate the quick response. Unfortunately, after hours I'm still not getting anywhere. Instead of foo.bars, which doesn't work, what syntax should I use to retrieve the bars field of the foo instance of Foo? Can you point me to the PureScript documentation where this particular feature I'm trying to implement is described?

monoidmusician

@oldfartdeveloper here’s four comparable options (the fourth requires the extra import and instance):

(\(Foo f) -> f.bars) foo

case foo of Foo f -> f.bars

let Foo f = foo in f.bars

import Data.Newtype (class Newtype, unwrap)

derive instance Newtype Foo _

(unwrap foo).bars

The first three are all different ways of pattern matching to unwrap the constructor, and the fourth is a magic typeclass instance to do the unwrapping with a generic function.

me

@monoidmusician Thank you. I'm just attempting to go down the 4th option. This really helps!

monoidmusician

happy to help 😊

pattern matching is one of my favorite features of PureScript and other languages: you can deconstruct data using almost the same syntax for constructing it!

me

Yeah, I need to practice it. I appreciate it but sometimes I don't recognize the opportunity.

monoidmusician

you can also deconstruct record literals, so you don’t even need the .bar accessor:

case foo of Foo { bar: b } -> b
-- with record puns:
case foo of Foo { bar } -> bar

you can also write getBarsFromFoo (Foo f) = f.bars

me

Here are your 5 options:

getBarsFromFoo :: Foo -> List Bar
getBarsFromFoo foo = (\(Foo f) -> f.bars) foo  

getBarsFromFoo2 :: Foo -> List Bar
getBarsFromFoo2 foo = case foo of Foo f -> f.bars

getBarsFromFoo3 :: Foo -> List Bar
getBarsFromFoo3 foo = let Foo f = foo in f.bars

getBarsFromFoo4 :: Foo -> List Bar
getBarsFromFoo4 foo = (unwrap foo).bars

getBarsFromFoo5 :: Foo -> List Bar
getBarsFromFoo5 (Foo f) = f.bars

Summary

I'm publishing this because I figure someone else will run into the same mistake of attempting to directly acceess a record field on a newtype (i.e. foo.bars).

The volunteers in the PureScript community are the most responsive and helpful of any of the software communities I've encountered over the many decades I've been a programmer. I waited too long to go for help on this, and I spun in the weeds for hours until I finally decided to create the test case and submit it. I'm so glad I did.

Finally, I've attached the test case, Foo.purs, if you'd like to download and try it for yourself.

Notes

  • This was done on purescript version 0.14.5
module Foo where
import Prelude
import Data.Newtype (class Newtype, unwrap)
import Data.Generic.Rep (class Generic)
import Data.List (List(..), filter, length, null)
import Data.List.Types ((:))
import Data.Show (class Show)
import Data.Show.Generic (genericShow)
newtype Foo = Foo { bars :: List Bar }
derive instance Newtype Foo _
derive instance Generic Foo _
instance Show Foo where
show = genericShow
newtype Bar = Bar { bar :: Int }
derive instance Generic Bar _
instance Show Bar where
show = genericShow
-- Code an example and see if I can retrieve bars
makeFoo :: Foo
makeFoo =
Foo { bars : ( (Bar { bar : 1 }) : Nil ) }
getBarsFromFoo :: Foo -> List Bar
getBarsFromFoo foo = (\(Foo f) -> f.bars) foo
getBarsFromFoo2 :: Foo -> List Bar
getBarsFromFoo2 foo = case foo of Foo f -> f.bars
getBarsFromFoo3 :: Foo -> List Bar
getBarsFromFoo3 foo = let Foo f = foo in f.bars
getBarsFromFoo4 :: Foo -> List Bar
getBarsFromFoo4 foo = (unwrap foo).bars
getBarsFromFoo5 :: Foo -> List Bar
getBarsFromFoo5 (Foo f) = f.bars
@oldfartdeveloper
Copy link
Author

Renamed README to make it show first

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment