evilsoft / crocks

A collection of well known Algebraic Data Types for your utter enjoyment.
https://crocks.dev
ISC License
1.59k stars 102 forks source link

Add a Tuple Type #269

Closed evilsoft closed 6 years ago

evilsoft commented 6 years ago

A simple Tuple type that lets us define a tuple of any size would be amazing. To get a constructor, the size of the tuple will need to be specified to get back an instance constructor, fixed to the size specified:

const Triple = Tuple(3)

// t :: Tuple String Boolean Number
const t = Triple('a', true, 97)

Implementation detail: When providing the constructor, we should return (up to Tuple-10) an anonymous function with the length of the function matching the size of the tuple. Providing the proper length for the function allows us to integrate with other libs curry and partial application helpers.

// suggested/possible implementation

function _Tuple(n) {
  if(!(isInteger(n) || n > 1)) {
    throw
  }
  const _type = () =>
     `${n}-Tuple`

  // instance impl
  function Tuple(n, ...parts) {
    if(parts.length !== n) {
       throw
    }

    // method defs

    return {
        inspect, toString: inspect, type: _type, concat, map,
        mapTuple, toArray, project, constructor: Tuple, merge
    }
  }

  Tuple['@@implements'] = () =>
    [ 'concat', 'map' ]

  Tuple.type = _type

  Tuple['@@type'] =
    `crocks/${n}-Tuple`

  switch(n) {
    case 1: return function(a) { return Tuple(n, a) }
    case 2: return function(a, b) { return Tuple(n, a, b) }
    case 3: return function(a, b, c) { return Tuple(n, a) }
    case 4: return function(a, b, c, d) { return Tuple(n, a, b, c, d) }
    case 5: return function(a, b, c, d, e) { return Tuple(n, a, b, c, d, e) }
    case 6: return function(a, b, c, d, e, f) { return Tuple(n, a, b, c, d, e, f) }
    case 7: return function(a, b, c, d, e, f, g) { return Tuple(n, a, b, c, d, e, f, g) }
    case 8: return function(a, b, c, d, e, f, g, h) { return Tuple(n, a, b, c, d, e, f, g, h) }
    case 9: return function(a, b, c, d, e, f, g, h, i) { return Tuple(n, a, b, c, d, e, f, g, h, i) }
    case 10: return function(a, b, c, d, e, f, g, h, i, j) { return Tuple(n, a, b, c, d, e, f, g, h, i, j) }
  }

  return function (...parts) { return _Tuple(n, ...parts) }
}

Instance Methods

Fantasy-land

Pointfree functions

evilsoft commented 6 years ago

All errors should read something like: 3-Tuple.map: Function required

Notice the n- in the type name

karthikiyengar commented 6 years ago

@evilsoft - I am trying to understand why we have to use the switch case to conditionally return functions. An alternate implementation follows.

function _Tuple(n, values) {
  if (n !== values.length) {
    throw new TypeError(
      `${n}-Tuple: Expected ${n} values, but got ${values.length}`
    )
  }

  const inspect = () => `Tuple(${values.map(_inspect).join(',')} )`

  return {
    constructor: _Tuple,
    inspect
  }
}

function Tuple(n) {
  if (!isInteger(n) || n < 1) {
    throw new TypeError('Tuple: Tuple size should be a number greater than 1')
  }
  return (...args) => _Tuple(n, args)
}

Also, this can be extended to work in a non-curried fashion (like Tuple(2, 1, 2) => Tuple(1, 2))

I see that you mentioned Providing the proper length for the function allows us to integrate with other libs curry and partial application helpers. I do see that doing something like Tuple(2).length with the above implementation would return 0. I'm trying to see use-cases where the length is important given that the type would be pre-curried.

If we do decide that accurate lengths are important for compatibility purposes, Object.defineProperty(fn, 'length', { value: n }) also does the trick while allowing arbitrary length Tuples.

Thoughts?

evilsoft commented 6 years ago

Well they should not be pre-curried. The types are not curried on their constructors. But if you look how ramda curries and the "short-path" for crocks curry, the length of the function comes into play: https://github.com/evilsoft/crocks/blob/master/src/core/curry.js#L17

This just allows our "short" path and will just work if a user uses ramda's curry to curry the tuple. If you look at all the types, the constructors always fully supply their inputs.

Does that make sense?

evilsoft commented 6 years ago

It also makes it easier (when not currying the constructor) for some IDEs to provide argument information about how the type is parameterized. If that makes sense.

evilsoft commented 6 years ago

I would say the lengths are important, but if you feel the defineProperty will work, give that a shot!

karthikiyengar commented 6 years ago

Ran a few quick tests, defineProperty works with curry 🎉

Another (probably obvious) question, but Tuple(2, 1, 2) => Tuple( 1, 2 ) should not be a valid way to construct right?

karthikiyengar commented 6 years ago

Also, regarding terminology - could we consider something like mapN or mapAll instead of mapTuple - It's possible that we would have similar types in the future and it'd be useful to have a generic name.

evilsoft commented 6 years ago

@karthikiyengar that makes sense, lets go with mapAll. correct about the it being invalid. We would like to let them know (at least with constructors) that there will be lost of information.

I am a little nervous about the length property, all other constructors have been explicit in their accepted arguments before. BUT I am willing to give it a shot, if you really do not want to do the switch/case thing.

evilsoft commented 6 years ago

@karthikiyengar after thinking about it for a bit, what would you think about nmap or mutimap?

karthikiyengar commented 6 years ago

@evilsoft - The idea behind getting rid of the switch case was due to the fact that as a JS lib, we're not limited by typesystem/other constraints. I've seen other language libraries (Scala Cats) limit it to 22, but mostly because they have helpers called map2, map3 and so on. That being said, I am completely open to doing switch/case, if what I suggested messes with consistency. Please let me know if I am missing something - for now Tuple.length => 1 and Tuple(5).length => 5 with my current implementation.

Regarding mapAll, sure, whatever works. As mentioned before, the Cats guys do mapN, not sure how how/whether the Haskell guys do it.

karthikiyengar commented 6 years ago

@evilsoft - Okay, on second thought, the switch/case looks like a better idea - I should have trusted your instinct 😄 - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty#Compatibility_notes

karthikiyengar commented 6 years ago

@evilsoft - Pushed a change that demonstrates some weirdness in writing the argument length check (eslint-disabled for now). Thoughts?

With respect to mapAll/mapTuple/nmap/multimap, should we have a point-free helper for this one as well? Also, we run into the same arity-length issues here, unless we switch/case this as well.

Also, I think we missed the equals property for this one in the description.

karthikiyengar commented 6 years ago

@evilsoft - Apart from the Tuple-10+ support, is there anything else needed to :shipit: ?

evilsoft commented 6 years ago

Shipped with 0.10.0. Closing