Closed user471 closed 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.
The problem is more with Eq
rather than with (==), because you cannot use any function with forall a. (Eq a)
constrain.
@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.
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.
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.
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.
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.
@garyb we have deriving for Generic. I don't think anyone is against getting deriving for Eq, Show, Ord :D
Ah yeah, definitely, I was just thinking of what's possible right now :smile:
@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?
Data.Array.deleteBy (eq `on` RecEq) x array
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.
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.
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?
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
You can't define objEqual
though and have it satisfy what equality is supposed to do.
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)
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.
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.
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.
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.
@natefaubion Do you also think that constructorless newtype cannot be implemented?
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.
@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.
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
.
@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.
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.
@user471 I would also say that newtypes really only exhibit (1) as an intrinsic property, and (2) is consequently possible by virtue of (1).
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.
The problem with instances for records in general is everything becomes an orphan, as these types are provided by the compiler.
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
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.
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.
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 :: _ }
.
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.
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.
If typename
acts like type
for the purposes of typing rules, then it has the same problems listed above.
@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.
Please explain how.
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
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
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. .
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.
@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.
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.
@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
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 newtype
s.
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 newtype
s at the at the PureScript level, and only differ by having no runtime representation.
Breaking this down:
newtype
to choose an instance for an operation. This applies to more than records (e.g. the Monoid
newtype
s). This is a valid issue, and in fact we briefly discussed it on IRC the other day.newtype
s 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 newtype
s, 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.
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.
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.
Closing as duplicate of #1510.
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? MaybeOr at least records could have default implementation of Eq, Ord, Show.