fantasyland / daggy

Library for creating tagged constructors.
MIT License
700 stars 29 forks source link

Possible usage inconsistency? #19

Closed dmitriz closed 7 years ago

dmitriz commented 7 years ago

Using the official example https://github.com/fantasyland/daggy#daggytaggedsumtypename-constructors

const Option = daggy.taggedSum('Option', {
  Some: ['x'],
  None: [],
})
const a = Option.Some(1) // { x: 1 }

But if I try the None part instead, I get error:

b = Opt.None()
TypeError: Opt.None is not a function
    at repl:1:9
    at sigintHandlersWrap (vm.js:22:35)
    at sigintHandlersWrap (vm.js:96:12)
    at ContextifyScript.Script.runInThisContext (vm.js:21:12)
    at REPLServer.defaultEval (repl.js:313:29)
    at bound (domain.js:280:14)
    at REPLServer.runBound [as eval] (domain.js:293:12)
    at REPLServer.<anonymous> (repl.js:513:10)
    at emitOne (events.js:101:20)
    at REPLServer.emit (events.js:188:7)

Instead it is supposed to be used as property, not function. So the way of use depends on the actual value of the array passed. If the array is empty, use as property. Otherwise - as function.

Is it intended?

On the other hand, if I use the tagged constructor directly, it is the function:

No = daggy.tagged('Nothing', [])
{ [Function]
  toString: [Function: typeRepToString],
  prototype: 
   { toString: [Function: tagged$toString],
     constructor: [Circular] },
  is: [Function: isType],
  '@@type': 'Nothing' }
> a = No()
{}

So now I pass the empty array exactly the same way, but this time I have to use it as function. And if I forget, the property itself is also a function, so I won't even get any error, but the result might be unpredictable.

So again, I am puzzled if that is how it is supposed to work.

safareli commented 7 years ago

for taggedSum if number of props is 0 then it should return a value and it's intentional. it's reflection of semantics of creating new data type In Haskell/PureScript.

I would be happy if we actually change implementation of tagged to:

const tagged = (name, props) => taggedSum(name, {[name]: props})

or just remove it.

the reason it's not quite possible to have same behaviour in tagged too, is that in case of taggedSum we are returning object which is Type Representative and it also holds variant constructors and values. but in case of tagged we are directly returning constructor which is also Type Representative of objects it's constructing.

Actually, technically we can return a value which is also Type Representative of itself:

const tagged = (typeName, fields) => {
  const proto = {}
  proto.toString = tagged$toString
  let typeRep
  if (fields.length) {
    typeRep = makeValue(fields, proto, [])
  } else {
    // this way we avoid named function
    typeRep = (0, (...args) => makeValue(fields, proto, args))
  }
  typeRep.toString = typeRepToString
  typeRep.prototype = proto
  typeRep.is = isType
  typeRep[TYPE] = typeName
  proto.constructor = typeRep
  return typeRep
}

But I would still vote for removing it or using taggedSum in it's implementation.

dmitriz commented 7 years ago

@safareli

for taggedSum if number of props is 0 then it should return a value and it's intentional. it's reflection of semantics of creating new data type In Haskell/PureScript.

I see, thank you for the explanation.

My Haskell experience is very basic, but I wonder how they distinguish between f as function and f() as function call with no arguments?

Another puzzling question (at least for me) resonating your suggestion to possibly deprecate it, is what does the dagger here bring over the sanctuary-def, that seems to address the same problem.

Quite confusingly, none of these two libraries seem to mention the other explicitly.

davidchambers commented 7 years ago

My Haskell experience is very basic, but I wonder how they distinguish between f as function and f() as function call with no arguments?

In Haskell every function takes exactly one argument. ;)

safareli commented 7 years ago

to give some intuition:

data Maybe a = Just a | Nothing

Nothing  :: Maybe a 
Just     :: a -> Maybe a 
Just 'c' :: Maybe Char 

data Unit = UnitVal
UnitVal :: Unit

data Boolean = True | False
True  :: Boolean
False :: Boolean

daggy helps you to create tagged constructors / sum types. sanctuary-def helps you to check types of values in Run-time. they are not related (at least to as much as sanctuary and ramda for example). https://github.com/sanctuary-js/sanctuary-union-type would be related once it's developed.

dmitriz commented 7 years ago

@davidchambers

In Haskell every function takes exactly one argument. ;)

Ah, that means they don't have this problem. :) But we are stuck with it in JS :(

dmitriz commented 7 years ago

@safareli

data Maybe a = Just a | Nothing

It might be just me, but I would read it as either Just function of one argument of Nothing function of no arguments. :)

Then if Haskell does not distinguish between the two, perhaps we cannot get any intiution to help us with this problem in JS.

This one is a bit confusing:

Nothing  :: Maybe a 

It does not seem to tell us whether Nothing is the same for all a, does it?

daggy helps you to create tagged constructors / sum types. sanctuary-def helps you to check types of values in Run-time. they are not related (at least to as much as sanctuary and ramda for example). https://github.com/sanctuary-js/sanctuary-union-type would be related once it's developed.

What about Sanctuary's type constructors like the Pair https://github.com/sanctuary-js/sanctuary-def/blob/v0.10.1/index.js#L639

Wouldn't it be not similar to daggy.tagged('Pair', ['first', 'second'])?

Sorry if I have missed anything.

safareli commented 7 years ago

But we are stuck with it in JS :(

You can just use taggedSum and then we would deprecated tagged or implement it using taggedSum

.... either Just function of one argument of Nothing function of no arguments. :)

There is no function of no arguments in haskell you either have a function or not. but you could have function which takes Unit (which has only one value (Boolean has two values)).

What's the point of having a pure function which has no argument and returns a value (if you want to delay computation of the value it makes sense but that's not the case for us). It's same as that same value. so you could just have that value.

From comments of the Pair

Constructor for tuple types of length 2. Arrays are said to represent tuples. ['foo', 42] is a member of Pair String Number.

Types defined in sacntuary-type are used for type checking. like the Pair you have linked takes two types one for each slot. and than it's used to validate if some value has shape of Pair and each slot contains valida value. like you can't do new Pair(1,2) or set some function on it's prototype. The Pair is Constructor types not for values.

dmitriz commented 7 years ago

@safareli Thank you for clarification. I see it also makes sense from the curried point of view:

// apply twice 
f = x => y => x + y
// apply once
f = x => x + 1
// apply 0 times
f = 1

I can see my confusion, it is really a union (or sum) of two separate object constructors, but the additional typecheck functionalities are what make the two libraries appear similar.

In a way, if I understand it correctly, it is a Sum (pun intended) of the type constructors for the union of the record types with typechecks, mixed with the constructor functions for each partial type. Where the cata function and the typechecks have of course more general scopes, not just for the records unions.

For instance, your type is either an Integer value or an Error record holding the error details. The cata and partial typechecks should work no differently in this situation.

I would find it helpful to put this library in a more general context.

Is that the direction behind that union-type project?