Closed alexeygolev closed 8 years ago
That should probably go into separate issue but I don't want to flood. I sketched out some additional things that could be useful. I haven't cleaned up the code. Wanted to get some feedback first. here is a gist Using it in this form in one of the 'non-toy' projects (don't tell anybody:))) PS. all the old tests are passing (after adding some api changes like .value and caseOf as well as some error RegExps)
This seems ambiguous to me. We passed an object to describe the fields so they can have no order?
@paldepind see the discussion here... it depends on browser implementation but as this suggests that it's in the specification now. So we can rely on the order properties were defined (we can put a disclaimer). Haskell does it the same way.
data Person = Person {
personName :: [Char]
,personId :: Int
} deriving(Show)
person1 :: Person
person1 = Person { personName = "John", personId = 1}
person2 :: Person
person2 = Person "Johnny" 2
Technically to align it with haskell we could create accessors for each property in the record (person.name(), person.id())
@alexeygolev Cool. I knew that browsers keep the property order. But I didn't know that relying on it was "allowed".
But what about this. It seem ambiguous as well. How does one know if this is an attempt at partially applying the value constructor or a wish to create a value from an object?
I think these ideas are really interesting and they seem very useful.
@paldepind Well it is kind of allowed but I guess adding a disclaimer would be nice as well. This way the lib user knows that there is something that needs attention. As for
let person2 = Person({id: 2, name: "Johnny"});
I think this is quite simple to reason about. If one defines his type using record syntax as in {key:String, anotherKey:String}
he can expect to create a value of this type either by using the same record syntax, or by using a simple way of just passing values in the order she defined the keys.
I found this really useful in Haskell. Sometimes your record values will be just two fields so it's handy to just use the simple notation. But sometimes it's easier to use the object notation. Notice though that I made sure that if you use record syntax to construct a value you can pass the keys in any order. Same way it is done in Haskell.
What I really like about this approach is that if we use records this way and have an ability to pass functions that will operate on the type we're defining, we can do things like this. Or, more importantly, make it possible for the lib user to do things like this if the wish so. Main api for accessing the value will still be accessing through an array. This way we're keeping a uniform consistent api for the lib but give the tools to extend it if needed. See the Maybe example...we basically have all the tools to create new monads without all the constructor functions, prototypes and new
madness.
I think this is quite simple to reason about. If one defines his type using record syntax as in {key:String, anotherKey:String} he can expect to create a value of this type either by using the same record syntax, or by using a simple way of just passing values in the order she defined the keys.
The problem is. If you create a type likes this: const Person = Type({Person:{parent: R.T, name: String, id: Number})
. Then you can instance it likes this:
var person = Person.Person(parent, name, id)
But since the value constructor is curried you could also do this:
var personFromId = Person.Person(parent, name)
And get back a constructor that only requires an id. But then, when you do this, ambiguity arises:
var personWithParent = Person.Person(parent)
How does the library know if this is an attempt at partially applying Parent
or an attempt to create a value from an object? We could use magic and simply treat any object with all the necessary keys as a value creation. But it still seems a bit unclean to me. Maybe having two different function would be better?
I found this really useful in Haskell. Sometimes your record values will be just two fields so it's handy to just use the simple notation. But sometimes it's easier to use the object notation. Notice though that I made sure that if you use record syntax to construct a value you can pass the keys in any order. Same way it is done in Haskell.
Yes. I certainly agree. What union type gives you today is something similar to Haskells data
type definition. With your proposal it would also achieve the same power as Haskells records.
What I really like about this approach is that if we use records this way and have an ability to pass functions that will operate on the type we're defining, we can do things like this. Or, more importantly, make it possible for the lib user to do things like this if the wish so. Main api for accessing the value will still be accessing through an array. This way we're keeping a uniform consistent api for the lib but give the tools to extend it if needed. See the Maybe example...we basically have all the tools to create new monads without all the constructor functions, prototypes and new madness.
Yes. This is certainly a nice feature. The only thing that might be missing is a way to extend an existing type with new methods. But one can of course create new functions that will work on existing types so that might not be an actual issue.
Now I get it. Sorry, completely misunderstood your previous question and went on about something else instead of properly answering it.
But since the value constructor is curried...
It's not
I have mixed feeling about partially applying the value constructor. I even catch one scenario of arguments length mismatch here. Would rather add a case for when we have less arguments than this type constructor expects.
But I see your point and if the aim is to align the ideas with Elm and Haskell we should support partial application. In Haskell, when using record syntax to construct a value you can't partially apply it. You either supply all the keys or you get the Fields of Person not initialised: personId
.
I will sketch something out today. It will be a bit more complex than just curryN.
Yes. This is certainly a nice feature. The only thing that might be missing is a way to extend an existing type with new methods. But one can of course create new functions that will work on existing types so that might not be an actual issue.
What kind of extensions do you have in mind? My idea of passing an object with functions was to somewhat mimic a typeclasses idea. It would just make dynamically dispatched functions like map in ramda work in a way a type creator would like it to work.
It's not
I did notice. But it currently is in union-type
and I think it is appropriate.
I'm thinking that if a type is created based on a object instead of an array an extra constructor function could be exposed with some postfix added like this:
let {Person, PersonOf} = Type({Person:{name: String, id: Number}})
var person1 = Person('Simon', 13)
var person2 = PersonOf({name: 'Alexey', id: 14})
This seems like a simple solution to me. The {name}Of
prefix is quite meaningless however, but I can't think of anything better.
What kind of extensions do you have in mind? My idea of passing an object with functions was to somewhat mimic a typeclasses idea. It would just make dynamically dispatched functions like map in ramda work in a way a type creator would like it to work.
Yes. I get that. I was thinking of a way to extend an existing type with new methods beyond those you initially add when you invoke Type
. You could add a new methods to it's prototype (the cons
object). But I don't actually think it's useful.
@paldepind I updated the code here. So far it works for the things I'm throwing at it. So we don't really need an extra PersonOf constructor. One thing I wanted to discuss though. When calling value on a value constructed with a type of another constructor (see eye colours) should we expand the values for the types embedded or should we only expand one level?
I think your ideas are awesome! But there are some things in the implementation that I am not too happy about.
Values are only accessible through value
or _value
. That is IMHO a big problem. If you create a type width some fields you expect to find the fields directly as properties of the type. I think that is a must for user friendliness and convenience.
I find the use of getters and setters confusing. Consider this example (it uses your latest version):
let {Person} = Type({Person: {name: String, id: Number}})
let me = Person({name: 'Simon', id: 13})
console.log(me.value) //=> ['Simon', 13]
// Natural expectation: I can set `value` with an array
me.value = ['Simon', 14]
console.log(me.value) //=> [undefined, undefined] – oops :(
So far it works for the things I'm throwing at it. So we don't really need an extra PersonOf constructor.
I still think we do. Consider this example (also using your latest version):
const T = () => true
let {Person} = Type({Person: {parent: T, name: String, id: Number}})
let father = Person(undefined, 'Knud', 12)
let createChild = Person(father) // Throws: TypeError: Fileds of Person not initialised: parent,name,id – oops
let me = createChild('Simon', 13)
Please take a look at this version in the "next" branch. It is my attempt at implementing your splendid ideas without sacrifices.
It has the following features:
if (process.env.NODE_ENV !== 'production')
. It should be possible to do plain browser friendly builds with envify.Of
postfixed method.One can do stuff like this:
function maybeMap(fn) {
var that = this;
return this.case({
Nothing: this.Nothing,
Just: function(v) { return that.Just(fn(v)); },
}, this);
}
var Maybe = Type({Just: [T], Nothing: []}, {map: maybeMap})
var just1 = Maybe.Just(1);
var just4 = just1.map(add(3));
// Nested destructuring
const T = () => true
let {Person} = Type({Person: {parent: T, name: String, id: Number}})
let father = Person(undefined, 'Knud', 12)
let createChild = Person(father) // Throws: TypeError: Fileds of Person not initialised: parent,name,id – oops
let me = createChild('Simon', 13)
let {parent: {name: fatherName}} = me
console.log(fatherName) //=> 'Knud'
The biggest problem in my implementation is that when you create a value from an object the fields will be put both in numerical properties (0
, 1
, etc.) and in the field name properties (name
, id
, etc.). I think not keeping numerical properties for record-like-types is the solution.
My code also doesn't support adding methods specifically to the values or to the type object. You can only add to both. That is fixable as well.
I also haven't implemented your toString
method. That is an omission (I forgot it).
Let me know what you think!
mmm...Iterators, yummy. Going to play with your code and let you know! I just want to know if you like the Of
for the API clarity or to avoid errors. I'm sure we can catch most of the scenarios like the one with undefined
above. I propose we create a new issue for the next
branch on continue there. This issue is growing way beyond the initial subject:)
I just want to know if you like the Of for the API clarity or to avoid errors. I'm sure we can catch most of the scenarios like the one with undefined above.
Both. And there will always be unambiguouty. We can also properly curry from the get go. This makes it possible to use Ramdas placeholders.
I propose we create a new issue for the next branch on continue there. This issue is growing way beyond the initial subject:)
Good idea!
I like how ramda team uses issues to bounce possible ideas off each other so figured we can try to do the same. I really like the lib overall but I think there are some things that could be improved. So here is an idea (I left the unchanged parts)
This way we basically only have values on our type instances instead of heaving everything on every instance. Thoughts?