purescript / purescript

A strongly-typed language that compiles to JavaScript
https://www.purescript.org
Other
8.55k stars 563 forks source link

Instances for records #1822

Closed user471 closed 8 years ago

user471 commented 8 years ago

The records are easy to use but at one point you need to create newtype and rewrite whole thing because you cannot compare two records. It's just weird that you cannot do {foo: 1} == {foo: 2} for example you can do it in Elm {foo = 1} == {foo = 2} Is it possible to do something about it? Maybe

newtype Foo = {foo :: Int}

Or at least records could have default implementation of Eq, Ord, Show.

natefaubion commented 8 years ago

As background, Elm implements == using a single function of type forall a. a -> a -> Boolean that does a universal structural equality of the arguments runtime representation (like lodash's isEqual). This can be handy, but it also means the == is broken (at runtime) for any of Elm's interesting container types that aren't implemented purely in Elm (Array, Set, etc) because they can be semantically equal, but their runtime representation can differ due to the order they by which they were constructed. There's no reason why one couldn't implement an FFI function with the same handyness (and pitfalls) as Elm's implementation.

user471 commented 8 years ago

The problem is more with Eq rather than with (==), because you cannot use any function with forall a. (Eq a) constrain.

garyb commented 8 years ago

@jonsterling wouldn't be my friend anymore if we allowed arbitrary instances for records.

Besides, there are many problems with overlapping instances and such like if we allowed people to define them. The workaround for now is to newtype. It can be done with a reasonably minimal amount of fuss, as you can generically derive the Eq instance for the newtype, and then do something like:

newtype RecEq a = RecEq { foo :: Int }
-- ... derive generic, make Eq instance with gEq ...

eqRec :: forall a. Eq a => { foo :: Int } -> { foo :: Int } -> Boolean
eqRec = eq `on` MyNewtype

So you don't have to manually wrap/unwrap the values.

natefaubion commented 8 years ago

To define a universal instance of anything for all records, you'd have to have some way of constraining all the labels to have that constraint, and then have some way of dispatching them (vararg constraints?). I think it probably makes more sense to just newtype and derive :P.

garyb commented 8 years ago

I should probably write a tool to generate common instances for record newtypes actually, as deriving can't solve every case (like if you use non-Generic instanced types in the record). I've written a few by hand recently and it would have been nice to have some way to generate them instead.

user471 commented 8 years ago

So you don't have to manually wrap/unwrap the values.

I changed one record to newtype and I had to use this wrap/unwrap in dozen places because of it. So in result I added bunch of useless code so I can only compare two records. Wraping/unwraping newtype is a main problem for me.

garyb commented 8 years ago

That's what I was suggesting above: a newtype that is only used to specialise the instanced operations, so you use the naked record type everywhere you need it, but then use the record-specific equality check in just the places it's needed.

natefaubion commented 8 years ago

@garyb we have deriving for Generic. I don't think anyone is against getting deriving for Eq, Show, Ord :D

garyb commented 8 years ago

Ah yeah, definitely, I was just thinking of what's possible right now :smile:

user471 commented 8 years ago

@garyb So, for example, I have

x = {foo:1}
array = [{foo:1}, {foo:2}]

And I want to use Data.Array.delete I need to wrap array, x, use delete then unwrap array back?

garyb commented 8 years ago
Data.Array.deleteBy (eq `on` RecEq) x array
garyb commented 8 years ago

I see what you're saying though, there are places where it's less than ideal to do so. I just think that the pain of those edge cases is worth having over broken equality for all records or other things like that.

user471 commented 8 years ago

Could you explain what is the main problem with

newtype Foo = {foo :: Int}
instance eqFoo :: Eq Foo where 
   eq Foo -> Foo -> Boolean
   eq x y = x.foo == y.foo

? The only difference is you don't need to use constructors.

garyb commented 8 years ago

What happens when someone makes a newtype Bar = { foo :: Int }? How do we tell whether a record that looks like that is Foo or Bar, and therefore which instance to use?

user471 commented 8 years ago

Data.Array.deleteBy (eqonRecEq) x array

It removes the point of Eq class, I can do the same with my universal operator

foreign import objEqual ::forall a. a -> a -> Boolean
infix 4 ===
(===) = objEqual
garyb commented 8 years ago

You can't define objEqual though and have it satisfy what equality is supposed to do.

user471 commented 8 years ago

How do we tell whether a record that looks like that is Foo or Bar, and therefore which instance to use?

you can always use (x::Foo)

user471 commented 8 years ago

You can't define objEqual though and have it satisfy what equality is supposed to do.

What do you mean? I have two js objects and I can compare them.

garyb commented 8 years ago

You can compare that they are referentially the same, not that they are the same value. In a language with immutable values referential equality is pretty much useless.

natefaubion commented 8 years ago

What do you mean? I have two js objects and I can compare them.

He means you can write that function, but there's no way to get the Eq dictionaries of any of its values. So you are bypassing any equality as defined by those instances.

user471 commented 8 years ago

He means you can write that function, but there's no way to get the Eq dictionaries of any of its values.

Yes, it can work only with objects which contains basic types.

user471 commented 8 years ago

@natefaubion Do you also think that constructorless newtype cannot be implemented?

garyb commented 8 years ago

It's not so much that you want a constructorless newtype, since that's exactly what it is at runtime, really you want implicit wrapping/unwrapping of newtypes. Ambiguity is the main problem with that, and also it ruins one of the primary cases of newtyping: to avoid mixing up basic types like String and Int by using newtypes to ensure that any random String can't be used for a Name, etc.

natefaubion commented 8 years ago

@garyb I don't think he's necessarily wanting implicit anything If he's happy with annotating it with a type, but that essentially boils down to safe, free coercions which just sounds like Roles. You can write a "free" Coerce a b class and a corresponding CoerceFunctor (f a) (f b) but there's no guarantee that it's safe.

user471 commented 8 years ago

to avoid mixing up basic types like String and Int by using newtypes to ensure that any random String can't be used for a Name, etc

What if you could use it only with records? Records already have constructors {foo:_} So when you use newtype it looks like you have double constructor for one type {foo:_} >>> Foo.

jonsterling commented 8 years ago

@user471 It sounds like an unjustified complication / special case...

My suggestion is to just wear the hair-shirt! :smile: I'm an ML programmer, so I'm used to it, but I think that as the type structure of PureScript continues to get more interesting, the bargain of type classes is going to get worse and worse, and folks are going to need to start taking alternatives seriously, even if the concrete notation will be a bit more abstruse.

user471 commented 8 years ago

Ambiguity is the main problem with that, and also it ruins one of the primary cases of newtyping: to avoid mixing up basic types like String and Int by using newtypes to ensure that any random String can't be used for a Name, etc.

By the way, newtype have two features 1) avoiding mixing up 2) ability to create instances. I need the second one, and I really don't want the first one. But they cannot be separated so I get the unwanted feature and I think this is the main problem.

jonsterling commented 8 years ago

@user471 I would also say that newtypes really only exhibit (1) as an intrinsic property, and (2) is consequently possible by virtue of (1).

user471 commented 8 years ago

I would also say that newtypes really only exhibit (1) as an intrinsic property, and (2) is consequently possible by virtue of (1).

But you can create instance for Int as well as for newtype MyInt = MyInt Int and fornewtype Foo = Foo {x:Int} but not for {x:Int} It feels not right.

natefaubion commented 8 years ago

The problem with instances for records in general is everything becomes an orphan, as these types are provided by the compiler.

user471 commented 8 years ago

The problem with instances for records in general is everything becomes an orphan, as these types are provided by the compiler.

What if module with type alias could be used as location of type definition?

type Foo = {foo :: Int} 
instance eqFoo :: Eq Foo where 
    eq Foo -> Foo -> Boolean 
    eq x y = x.foo == y.foo
paf31 commented 8 years ago

Moving to Discussion.

My justification for not allowing record instances was always that no module should have the privilege of deciding the behavior assigned to a particular label or set of labels. This is the point of extensible records, effects, etc. Maybe I can state that more formally, but hopefully you understand the intent. If module A decides that (foo :: _) has a particular equality, then module B is stuck with that definition, even if foo means something else there.

I'd love to see Eq and Ord deriving, but Show is a bit sketchy due to the lack of showsPrec right now.

user471 commented 8 years ago

This is the point of extensible records, effects, etc. Maybe I can state that more formally, but hopefully you understand the intent. If module A decides that (foo :: _) has a particular equality, then module B is stuck with that definition, even if foo means something else there.

I understand it. That is why I suggested

newtype Foo = {foo :: Int}

So the record type could have name, and you don't create instance for any {foo::_} but for specific one.

I'd love to see Eq and Ord deriving

Maybe it would solve most of the cases.

user471 commented 8 years ago

Maybe I can rephrase this idea. Could record type have real name and not just alias? something like

typename Foo = {foo  :: Int}

So typename could be used for creating instances. Instance can only be used if compiler can figure out the typename and cannot be use for any {foo :: _ }.

paf31 commented 8 years ago

So typename could be used for creating instances

I don't understand the difference.

In general, I'd prefer to discuss these things on IRC before making issues. It adds overhead to track these kinds of issues over time.

user471 commented 8 years ago

I don't understand the difference.

Right now there are type and newtype. Type cannot be used to restrict types

type Foo1 = {foo::Int}
type Foo2 = {foo::Int}
x :: Foo1
x = {foo: 1}
y :: Foo2
y = x

newtype can, but it's too clumsy, because you need to use constructor everywhere. So, there could be something between them, so you would be able to restrict types but you don't need to use constructors.

typename Foo1 = {foo::Int}
typename Foo2 = {foo::Int}
x :: Foo1
x = {foo: 1}
y :: Foo2
y = x --compiler error

this could be used to create record instances

instance eqFoo :: Eq Foo where ...

I'd prefer to discuss these things on IRC before making issues.

I will try next time.

paf31 commented 8 years ago

If typename acts like type for the purposes of typing rules, then it has the same problems listed above.

user471 commented 8 years ago

@paf31 You said that the only problem with record instance is

that no module should have the privilege of deciding the behavior assigned to a particular label or set of labels.

typename doesn't have such problem.

paf31 commented 8 years ago

Please explain how.

natefaubion commented 8 years ago

I think this is being said

-- coercion introduces a fresh nominal type
coercion Foo = { foo :: Int }

rec :: { foo :: Int }
rec = { foo: 42 }

-- coerce is a builtin operation that tries to resolve a coercion
-- between two types by substituting `coercion`s and seeing
-- if they are identical
foo = coerce rec :: Foo
foos = coerce [rec] :: Array Foo
user471 commented 8 years ago

Please explain how.

Because the instance is for type Foo and not for type {foo::Int}

module A where
typename Foo = {foo::Int}
instance eqFoo :: Eq Foo where 
    eq Foo -> Foo -> Boolean 
    eq x y = x.foo == y.foo
module B where
import A
type Bar = {foo:: Int}

This wouldn't work because there isn't Eq for {foo::Int}

test :: Bar -> Bar -> Boolean
test x y = x == y  -- error NoInstanceFound

But this would

test :: Foo -> Foo -> Boolean
test x y = x == y 
user471 commented 8 years ago

Another thought. So, if you need instances you need to use newtype istead of pure record

newtype Bar = {bar :: Int}
newtype Foo = {foo :: Bar}

And you lose all record features because of it. You cannot use dot notation, update syntax, there isn't row polymorphism. It feels like PureScript made a step to separate record from newtype but didn't figure out how to make it first class citizen. .

LiamGoodacre commented 8 years ago

I don't think it really makes sense for records to have instances, however I agree that records usages don't play well with things like newtypes. What are everyone's thoughts on extending record getter/setter paths to include newtype constructor names when in scope? For example:

-- example exporting all constructors
module A0 (Foo(..), Bar(..)) where
newtype Foo = F {foo: Bool}
newtype Bar = B {bar: Foo}

module A1 where
import A0
-- B and F are in scope, so they can be used in record paths

getter :: Bar -> Bool
getter = _.B.bar.F.foo

setter :: Bool -> Bar -> Bar
setter b = _ { B.bar.F.foo = b }

setterF :: {foo: Bool} -> Bar -> Bar
setterF f = _ { B.bar.F = f }
-- example not exporting `F`
module B0 (Foo(), Bar(..)) where
newtype Foo = F {foo: Bool}
newtype Bar = B {bar: Foo}

module B1 where
import B0
-- only constructor B is in scope

getter :: Bar -> Bool
getter = _.B.bar.F.foo  -- compile error, unknown newtype constructor F

setter :: Bool -> Bar -> Bar
setter b = _ { B.bar.F.foo = b } -- same error

setterF :: {foo: Bool} -> Bar -> Bar
setterF f = _ { B.bar.F = f } -- same error

That is, if the compiler sees a newtype constructor in a record path it can read the wrapped data type if it is in scope.

zudov commented 8 years ago

@LiamGoodacre This would be quite a nice syntax for what is now conventionally handled with run*.

In case of getter it's very similar:

getter = runB >>> _.bar >>> runF >>> _.foo

But in case of setter, it of course becomes very unwiedly, that's why I often have to define _B lens (for focusing on the record inside of newtype), and then a bunch of lenses named as fields that are like foo = lens _.foo (_ { foo = _ }).

With such addition one wouldn't have to lift lenses so often, which seem like a good thing.

zudov commented 8 years ago

Also, Lens type depends only on Functor, it would be pretty cool to have anonymous lenses that focus on record fields. This way we wouldn't need to have syntax features for things that can be more flexibly implemented in a library.

user471 commented 8 years ago

@LiamGoodacre

What are everyone's thoughts on extending record getter/setter paths to include newtype constructor names when in scope?

But it doesn't solve the problem with row polymorphism.

@zudov

that's why I often have to define _B lens (for focusing on the record inside of newtype)

Why not use

lens (\(Foo x) -> x.foo) (\(Foo x) v -> Foo x { foo = v })

instead of

lens _.foo (_ { foo = _ })

so you don't need to focus on records.

The only problem is fields with the same name, because you cannot use row polimophism with newtype.

Generalizing the LiamGoodacre's idea. Maybe type system could have something like constructor wildcard

newtype Foo = Foo {x :: Int}
newtype Bar = Bar {x :: Int}
getX :: forall a. (* {x :: Int|a}) -> Int
getX obj = obj._.x
setX obj v = obj._ { x = v}

getX setX only accept newtypes that have constructor {x :: Int|a}

It could be used not only with records

run:: forall a. (* a) -> a
run (_ x) = x
garyb commented 8 years ago

What could run possibly mean? It looks like "extract something from a type kinded * -> *" but I'm pretty sure that's not your intention, as the rest of this is about newtypes.

The problem here is these suggestions are a bunch ad hoc partially though out ideas to solve one inconvenience that already has a workaround that doesn't involve manging the entire language.

When adding features to a language you have to consider every possible effect they might have, not just one situation where it would be nice to have, and so far every proposal in here has bad or unspecified behaviour outside of one special case. Or it only operates in that one special case, which is also not good. In my opinion a language should have as small a syntax as is practical. The more general a syntax, the more useful and interesting ways it can be combined.

newtype is an optimization primarily, introduced specifically to solve the problem we're talking about, as well as for allowing disambiguation of values by preventing their types from being mixed up. I say it's an optimization, as the same effect can be achieved by defining types with a single constructor and argument, and until we introduced newtype, that's exactly how you achieved this. The semantics of a one-constructor-one-argument types are idential to newtypes at the at the PureScript level, and only differ by having no runtime representation.

Breaking this down:

  1. There are two conflicting goals here: "naked" records are anonymous types, class instances are globally unique, so we are never going to allow instances for records.
  2. It's sometimes inconvenient to inject/project a newtype to choose an instance for an operation. This applies to more than records (e.g. the Monoid newtypes). This is a valid issue, and in fact we briefly discussed it on IRC the other day.
  3. We'd like a better solution that either works for all newtypes containing any value, or for when there is an ambiguity between class instances (currently this latter case is disallowed entirely).

The only suggestion I've seen so far in here that kinda fits is perhaps a way of annotating types that specifically says "coerce this thing", but I think it would require some trickery in the type system to make it possible, as ideally you'd want to be able to coerce "deep" values, something like:

things :: Array (Either String (Tuple { foo :: Int }, Boolean)) 
coercedThings = things ::~ Array (Either String (Tuple SomeRecordNewtype, Boolean))

and it would only be applicable to newtypes, although this would make them more distinct from one-constructor-one-value types.

The fatal flaw I see in this suggestion is how it would play in the case where newtypes hide their constructors from export in favour of smart constructors to enforce some invariant when a value is being created; if you can coerce any newtype with the type system then this important ability is lost.

I've already spent far too much time on this though, so I'll leave it at that.

natefaubion commented 8 years ago

I think the fatal flaw with "deep" coercions is that you can break data structure invariants that depend on an instance (like Set with Ord). It's really only applicable when converting an anonymous type to a nominal type, since they can't have instances. If you could coerce nominal types, or coerce in both directions, it would be very unsafe.

user471 commented 8 years ago

It's not so much that you want a constructorless newtype, since that's exactly what it is at runtime, really you want implicit wrapping/unwrapping of newtypes. Ambiguity is the main problem with that, and also it ruins one of the primary cases of newtyping

What if you could separate these two cases?

implicit newtype Foo = Foo {foo :: Int}

What disadvantages would it have?

I think the fatal flaw with "deep" coercions is that you can break data structure invariants that depend on an instance (like Set with Ord)

Could it solve this problem? If you data have some invariant then you just don't use implicit keyword.

paf31 commented 8 years ago

Closing as duplicate of #1510.