I would :heart: a Haskell-like Show type class. Useful functions such as R.toString, S.toString, and Z.toString rely on toString methods, but there's a problem with this approach:
// Person :: (String, String, String) -> Person
function Person(firstName, lastName, emailAddress) {
if (!(this instanceof Person)) return new Person(firstName, lastName, emailAddress);
this.firstName = firstName;
this.lastName = lastName;
this.emailAddress = emailAddress;
}
// Person#toString :: Person ~> () -> String
Person.prototype.toString = function() {
return '"' + this.firstName + ' ' + this.lastName + '" <' + this.emailAddress + '>';
};
In this case, Person#toString serves some other useful role.
It would be nice to be able to define this as well:
// Person#fantasy-land/show :: Person ~> () -> String
Person.prototype['fantasy-land/show'] = function() {
return 'Person(' + Z.show(this.firstName) + ', '
+ Z.show(this.lastName) + ', '
+ Z.show(this.emailAddress) + ')';
};
With a suitable definition for Person#fantasy-land/equals, one could then write:
> var puffnfresh = Person('Brian', 'McKenna', '[email protected]')
> Z.equals(eval(Z.show(puffnfresh)), puffnfresh)
true
I've found myself giving a function a Showable a constraint from time to time, but this is currently almost meaningless as the presence of a toString method does not imply eval(show(x)) = x.
Interesting. A few questions that come to mind.
Why do you think this is inside the scope of fantasy land? There are many useful interfaces but they can't all be inside the scope of fantasy land.
Can that law actually be guaranteed? For instance the show method on Person assumes that Person is in scope. But that may not be the case if the library is imported qualified. I.e. if person is imported as lib.Person then calling eval on a person.show() will throw an error.
Why is such a method useful? If the goal is to convert a value to a string and back again why not define an interface with a serialize and deserialize methods? Then it should be the case that obj.deserialize(obj.serialize()) === obj. That would work without relying on certain names being in scope and the other problems associated with eval.
Thank you for your feedback, @paldepind. :)
Why do you think this is inside the scope of fantasy land?
It's currently the place for defining type classes. I suppose we could specify sanctuary/show in sanctuary-type-classes. Do you think this would be more natural?
Can that law actually be guaranteed?
No. Laws are not guaranteed, even in Haskell. The "laws" for Show are really just recommendations (even in Haskell).
Why is such a method useful?
In a word, logging! Which of these would you prefer to see when debugging?
Right({"x": {"y": {"z": 42}}})
{ isLeft: false,
isRight: true,
value: { x: { y: [Object] } },
'fantasy-land/concat': [Function: Either$prototype$concat],
'fantasy-land/equals': [Function: Either$prototype$equals],
'fantasy-land/lte': [Function: Either$prototype$lte] }
It's also useful to have a REPL output representations which are acceptable inputs.
If the goal is to convert a value to a string and back again why not define an interface with a
serializeanddeserializemethods?
The goal is to have succinct string representations for display in REPL output, sanctuary-def error messages, and log messages. I realize that I did not make this clear initially! The eval(show(x)) = x property is intended to encourage library authors to format these string representations consistently.
Here's an example (from sanctuary-js/sanctuary-def#159) of a function which can be written with a Show/Showable constraint:
// unique :: Showable a => Array a -> Array a
function unique(xs) {
var strMap = {};
xs.forEach(function(x) { strMap[Z.toString(x)] = x; });
return Object.keys(strMap).map(function(k) { return strMap[k]; });
}
This is very much a secondary benefit of specifying a Show/Showable type class.
@davidchambers
Thank you for explaining. If I understand correctly, actually using the output with eval is not the primary goal. It's just a guideline for implementing the show method. I can definitely see how such a method can be useful 馃槃 . It's purpose does lies very close to the toString method though.
It's currently the place for defining type classes.
I would argue that what Fantasy Land specifies is not type classes but interfaces. It specifies methods on objects. That is exactly what many languages calls "interfaces". Type classes, on the other hand, are something else. It may seem like nitpicking, but I think it's an important distinction to make. Type classes and interfaces are different in ways that make a difference for several of the Fantasy Land abstractions. At leas monoids and applicatives. Therefore I think that calling what Fantasy Land defines "type classes" is misleading. "Interfaces" fit the bill.
I suppose we could specify sanctuary/show in sanctuary-type-classes. Do you think this would be more natural?
Yes, I think so. I think the scope of Fantasy Land is to define interfaces for algebraic structures. That is also what the readme seems to imply. There are practically an unlimited number of possible interfaces so putting the line somewhere is nececarry.
I agree that "interfaces" is a more accurate descriptor than "type classes" (although I'm somewhat wedded to the latter due to prior naming decisions).
If the specification for the Showable interface is to live somewhere in the @sanctuary-js account, I suggest the following:
@@show :: T ~> () -> String (much as we did for @@type).show :: Showable a => a -> String.Z.toString from sanctuary-type-classes.S.toString to S.show, and depend on our new package.@Avaq, how does this sound to you?
I get a bit worried at 4 and 5. Will S.show continue to provide sane @@show implementations for primitives and built-ins?
I like @paldepind's idea with serialize/deserialize. What about toString/fromString for the round-trip? Ideally fromString would give you a Maybe a or Either Error a or something.
I get a bit worried at
4and5. WillS.showcontinue to provide sane@@showimplementations for primitives and built-ins?
Nothing would change in this respect. We'd simply move Array$prototype$toString and friends to the new package and replace toString with show.
I like @paldepind's idea with
serialize/deserialize.
I now realize the issue description is misleading. I should have made it clear that eval(show(x)) = x is a guide for producing useful string representations for logging and for REPLs. It is not intended to be used for serialization (though it may occasionally be suitable for this purpose).
sanctuary-show now exists. :tada:
Most helpful comment
@davidchambers
Thank you for explaining. If I understand correctly, actually using the output with
evalis not the primary goal. It's just a guideline for implementing theshowmethod. I can definitely see how such a method can be useful 馃槃 . It's purpose does lies very close to thetoStringmethod though.I would argue that what Fantasy Land specifies is not type classes but interfaces. It specifies methods on objects. That is exactly what many languages calls "interfaces". Type classes, on the other hand, are something else. It may seem like nitpicking, but I think it's an important distinction to make. Type classes and interfaces are different in ways that make a difference for several of the Fantasy Land abstractions. At leas monoids and applicatives. Therefore I think that calling what Fantasy Land defines "type classes" is misleading. "Interfaces" fit the bill.
Yes, I think so. I think the scope of Fantasy Land is to define interfaces for algebraic structures. That is also what the readme seems to imply. There are practically an unlimited number of possible interfaces so putting the line somewhere is nececarry.