zenika-open-source / immutadot

immutadot is a JavaScript library to deal with nested immutable structures.
https://immutadot.zenika.com
MIT License
178 stars 5 forks source link

Better currying #297

Closed nlepage closed 6 years ago

nlepage commented 6 years ago

It would be nice to find a better way of managing optional currying. For now the type of the first arg is tested, if it is a string, curried version is assumed otherwise non curried version. This is not entirely satisfying, this forbids to use arrays as paths (though it would be nice). Something based on the number of parameters could be used in some cases, but if there is any optional parameter this is not possible... Also is currying only the first parameter a good choice ?

fabienjuif commented 6 years ago

I was reading your API yesterday and was perplex of this currying implementation choice :)

I had some question like that before for others libs, and I didn't find a good solution. The argues will be interesting here! 👍


What I do for now is this:

const fn = (options = {}) => required1 => required2 => {
  const { optional1, optional2 } = options
  return required1 + required2
}

caveats: this is boring to write and empty () when we don't have options no currified: fn(required1, required2, options)

We can also do this:

const fn = (required1, options = {}) => (required2, options2 = {}) => {
  const { optional1, optional 2}  = { ...options, ...option2 }
  return required1 + required2
}

caveats: the API is hard to understand, WHY there is muliple options ? not currified: fn(required1, required2, options)

This is not exactly what you are looking for, but maybe the idea of grouping optionals parameter can help you find a better idea.

nlepage commented 6 years ago

Hey Fabien,

Thanks for your hints, this is the first open discussion we have on currying in this project.

Optionally currying the first parameter was only motivated by flow():

flow(
  set('nested.a', 'foo'),
  set('nested.b', 'bar'),
)(obj)

I think this is important to keep this, it's a simple and expressive way of grouping different operations on one object.

Adding more currying is not necessary, immutadot is not branded a functional library, but it's always nice.

Our main problem is how do I know if I should curry, without testing the first parameter's type ?

One way could be to force the user to make a first call with a number of parameters lower than the minimum arity of the function in order to "trigger" currying. As soon the minimum arity minus one is reached, use the maximum arity to know if obj has already been sent, otherwise return a last function to receive obj.

fill() has a minimum arity of 3 and a maximum arity of 5, this would allow to do this:

// No currying
fill(obj, 'nested.array', 'foo')
fill(obj, 'nested.array', 'foo', 1)
fill(obj, 'nested.array', 'foo', 1, 3)
// Currying
fill('nested.array', 'foo')(obj)
fill('nested.array')('foo')(obj)
fill('nested.array')('foo', 1)(obj)
fill('nested.array')('foo', 1, 3)(obj)
fill('nested.array')('foo', 1, 3, obj)
// Invalid currying
fill('nested.array')('foo', obj) // obj would be mistaken for the first optional parameter
fill('nested.array')('foo', 1, obj) // obj would be mistaken for the second optional parameter

The last function could even receive optional parameters, but this is a little weird and doesn't fit with flow():

fill('nested.array', 'foo')(1, obj)
fill('nested.array', 'foo')(1, 3, obj)
fill('nested.array')('foo')(1, obj)
fill('nested.array')('foo')(1, 3, obj)
fill('nested.array')('foo', 1)(3, obj)
fabienjuif commented 6 years ago

Why don't you want to make immutadot a full currified lib ?

(required, options) => (required2, options2)


The solution you speak about close a door : it would not be possible to have an object as a parameter (even an array ?)

nlepage commented 6 years ago

The solution I'm suggesting is only based on the number of parameters received, the type of the parameters doesn't matter, it may be objects, arrays, undefined, whatever...

The examples I give don't use any objects apart from obj, but it's just a coincidence.

In fact I'm not a big fan of options objects :sweat_smile:

fabienjuif commented 6 years ago

Maybe you should ask a scala/haskell dev how it would handle optional parameters ?

ping @EmrysMyrddin

nlepage commented 6 years ago

I made a PoC of what I'm suggesting in #298.

Still with fill()'s example, minimum arity of 3, eventually the rules are rather "simple":

nlepage commented 6 years ago

@hgwood may also have an opinion on this

EmrysMyrddin commented 6 years ago

I don't like the last solution consisting in having optional parameter in the last curryfied function call.

The problem with this is that you can't use your function with native array API such as map or foreach that doesn't doesn't call the callback only with the current value but also with the index and the entire array.

array.map(fill('nested.attribute', 'hello world'))

This example will have some unexpected behaviour.

nlepage commented 6 years ago

@EmrysMyrddin Yes, agreed on this, we will force the last call to be unary with only obj, this was already the case in v1.

Just for the record your example can be done like this:

fill(array, '[:].nested.attribute', 'hello world')

After some more discussions with @frinyvonnick, we think not allowing to put obj with other parameters would be more consistent, ie forbid this:

fill('nested.array')('foo', 1, 3, obj)

We also discussed whether forcing to make a first unary call with only path or not was a good idea, @frinyvonnick wanted to have a more simple rule on how to make a curried call:

Make a first call with a number of parameters lower than minimum arity

vs

Make a first call with only path

We had reached an agreement on not doing it, but doing it in the examples of the documentation.

After some more thinking on my side I think it's not a good idea, and we should not make all our examples like this. This would be a major breaking change in the usage of flow() (going from set('foo.bar', 'aze') to set('foo.bar')('aze')). In a large amount of cases, optional parameters (if any) are not used, so the rule make a first call with a number of parameters lower than minimum arity is implicit, you just have to omit obj.

nlepage commented 6 years ago

I have update the PoC #298.

It is not necessary to give the maximum arity anymore.

The minimum arity may be omitted, it defaults to fn.length.

nlepage commented 6 years ago

closed by #298