Picolab / pico-engine

An implementation of the pico-engine hosted on node.js
http://picolabs.io/
MIT License
43 stars 7 forks source link

Think through stdlib and how KRL should polymorphically treat types #235

Open farskipper opened 7 years ago

farskipper commented 7 years ago

Right now KRL, stdlib operator functions are treated as syntactic sugar for function calls.

foo.put(bar, baz)

//effectively compiles to

stdlib("put", foo, bar, baz)

Therefore any stdlib operator can be used on any type. i.e. operators are not behaving like method calls (which is what they look like)

One downside of the current syntax is it looks like you should be able to define your own operator (method) i.e.

blah = {
    "add": function(a, b){ a + b }
}

blah.add(1, 2)//ERROR

//.. but
blah["add"](1, 2)// works

One option is to drop the .key membership access so that . is only used for operators.

Since operators can be used on any type, the standard library tries to treat values polymorphically where possible (in a half-baked way). For example maps and arrays. Arrays are treated as maps with integer keys.

For example:

["a", "b", "c"].keys()
// => [0, 1, 2]

foo = {
    "aaa": 100,
    "bbb": 200,
    "ccc": 300
}

foo.length()
// => 3

foo.filter(function(value, key){
   return value > 100;
})
// => {"bbb": 200, "ccc": 300}

foo.all(function(value, key){
   return value < 999;
})
// => true

So iteration functions take the signature function(value, key, collection){..} This is the same for maps and arrays. And even the same for the foreach on rules where setting(value, key) and you can foreach over maps or arrays.

However, this has not been totally thought through. For example using .head() or.tail() on a map. Should it use values, keys, or key/value pairs?

foo = {
    "aaa": 100,
    "bbb": 200,
    "ccc": 300
}

foo.head()
// => 100
// => "aaa"
// => ["aaa", 100]

foo.tail()
// => [200, 300]
// => ["aaa", "bbb"]
// => [["bbb", 200], ["ccc", 300]]
// => {"bbb": 200, "ccc": 300}

If we just use values, it will be consistent with how iterator functions use the value as the first argument. And if you wanted pairs you could do foo.pairs().head()

An alternative approach is to treat maps as lists of key value pairs. Instead of treating arrays as maps, treat maps and arrays both as lists. For example:

foo.filter(function(value, key){
   return value > 100;
})
//becomes
foo.filter(function(pair){
   return pair[1] > 100;
})
//if KRL support destructuring, then
foo.filter(function([key, value]){
   return value > 100;
})

This is more similar to how clojure treats all collections as all seqs.

Anyways, sorry for the novel. This is just to begin a discussion. I believe thinking through all this will help reduce all the confusion/bugs surrounding the stdlib, and make it quicker to implement it right.

farskipper commented 7 years ago

Related: #171 think about user-defined KRL operators

0joshuaolson1 commented 7 years ago

Great start for discussion.

option is to drop the .key membership access

I support this.

I vote to keep maps and arrays separate. You'd basically need to check if a list is an array type for certain operators.

0joshuaolson1 commented 7 years ago

I noticed that the http library docs use ["<key>"] to access a map value. I don't think this should be supported for maps, because we already have {"<key>"}, which can include a hashpath.

farskipper commented 7 years ago

Related is being consistent on KRL's handling of type errors. I know in the old language and much of the new KRL is weakly typed/does type coercion where possible. However, other places where type coercion doesn't work should we return null or raise a TypeError?

i.e. #201 n.range("-1") should that try and parse the string to an int first? then what about n.range("foo") ? coerce to 0, return null, or raise an error?

i.e. null + 4 should it be 4 , "null4" or raise an error?

0joshuaolson1 commented 7 years ago

any stdlib operator can be used on any type. i.e. operators are not behaving like method calls

The iterator functions, for example, could check the type (array or map) and have a different implementation for each, i.e. dynamic dispatch.

And so we could forbid .head() and .tail() on maps, which isn't generally catchable at compile time, but we could throw runtime errors.

where type coercion doesn't work should we return null or raise a TypeError?

With the exception of methods like .as() that explicitly deal with type conversion (so null sometimes makes sense), I think type errors should be type errors, and implicit coercion should be explicitly documented for the library apis.

Which is how I recommend handling the .range() case.

0joshuaolson1 commented 7 years ago

Note that KRL's stance on weak typing/implicit coercion still lets us decide what type errors really are unfixable by coercion.

0joshuaolson1 commented 7 years ago

Let's let maps have key ordering in the JS way (and giving more compatibility with arrays)

0joshuaolson1 commented 7 years ago

In light of the map ordering decision, @farskipper would understand the implications of this better than me:

http://blog.caplin.com/2012/01/13/javascript-is-hard-part-1-you-cant-trust-arrays/

0joshuaolson1 commented 7 years ago

I'm not sure about map key order: https://stackoverflow.com/questions/30076219/does-es6-introduce-a-well-defined-order-of-enumeration-for-object-properties/30919039#30919039

>< uses map keys, collection operators use map values; if head/tail works I prefer map values, but I haven't found anything about plain object insertion order in the ES5 spec yet.

0joshuaolson1 commented 7 years ago

Change of plans: KRL will not guarantee map key order or support <map>.head() or tail.

0joshuaolson1 commented 7 years ago

@farskipper BTW, I found out about object enumeration in the ECMAScript spec, and Lodash uses it: https://github.com/lodash/lodash/blob/860d1f9484982d45ead630f3ed87ca7d0c5ae2cd/_baseAssignValue.js#L16

0joshuaolson1 commented 6 years ago

However, async makes it sound undependable (or new?).