further icon indicating copy to clipboard operation
further copied to clipboard

Resolve constructor defaulting

Open jem-computer opened this issue 8 years ago • 16 comments

// Style :: => (Props -> CSS) -> Style CSS
function Style(f) {
  if (!(this instanceof Style)) {
    return new Style(f);
  }

  this.__value = typeof f === "function" ? f : () => f;
}

i'm not a fan of this defaulting. IMO either always take a function or change the type sig to describe what legal arguments are (namely anything) @buzzdecafe

I think this is due to .of

Would this be better @buzzdecafe?

 // Style :: => (Props -> CSS) -> Style CSS
 function Style(f) {
   if (!(this instanceof Style)) {
     return new Style(f);
   }

-  this.__value = typeof f === "function" ? f : () => f;
+  this.__value = f;
   return this;
 }
 // .of :: Applicative a -> Style a
 Style.prototype.of = function(a) {
-  return new Style(a);
+  return new Style(_ => a);
 };

jem-computer avatar Mar 15 '17 04:03 jem-computer

So this depends on if we want to support Style.of({ foo: "bar" }) or require Style.of(_ => ({ foo: "bar" })). To keep with fantasy-land spec, we can't check the value when using of:

No parts of a should be checked (from spec).

This is true for static land checks

jbaxleyiii avatar Mar 15 '17 17:03 jbaxleyiii

I'd rather change the type signature to be accurate 👍

jbaxleyiii avatar Mar 15 '17 17:03 jbaxleyiii

I don't understand "No parts of a should be checked"

I was imagining this similar to fluture's of - i.e. creating a Style that always resolves to the provided value, ignoring the props you resolve it with:

.of :: a -> Future _ a

Creates a Future which immediately resolves with the given value. This function is compliant with the Fantasy Land Applicative specification.

const eventualThing = Future.of('world');
eventualThing.fork(
  console.error,
  thing => console.log(`Hello ${thing}!`)
);
//> "Hello world!"

jem-computer avatar Mar 15 '17 17:03 jem-computer

@jongold I was reading the spec saying the .of method can't do the checking. But the constructor can?

Like https://github.com/fluture-js/Fluture/blob/master/fluture.js#L136-L143

So it looks like future.of returns a FutureOf which implements Future. Can you call Future.of(_ => a)?

If not, Style.of could only take an object and Style() could only take a function? Using the same logic.

jbaxleyiii avatar Mar 15 '17 17:03 jbaxleyiii

That was the behavior I was imagining, yeah.

The ramda-fantasy Future.of implementation is simpler —

// applicative
Future.of = function(x) {
  // should include a default rejection?
  return new Future(function(_, resolve) { return resolve(x); });
};

jem-computer avatar Mar 15 '17 17:03 jem-computer

Perfect! I had that originally haha. Can do!

On Wed, Mar 15, 2017, 1:40 PM Jon Gold [email protected] wrote:

That was the behavior I was imagining, yeah.

The ramda-fantasy Future.of implementation is simpler —

// applicative Future.of = function(x) { // should include a default rejection? return new Future(function(_, resolve) { return resolve(x); }); };

— You are receiving this because you commented.

Reply to this email directly, view it on GitHub https://github.com/jongold/st/issues/6#issuecomment-286821807, or mute the thread https://github.com/notifications/unsubscribe-auth/AEvMsA4RhZehB8_XA52Cnd3p0qNxOGaLks5rmCJ0gaJpZM4Mde4F .

jbaxleyiii avatar Mar 15 '17 17:03 jbaxleyiii

i reserve the right to contradict myself. So here goes. I believe I was in error. I think the "lawful" approach is that the Style constructor can do the wrapping when necessary, as it is now more or less:

function Style(f) {
  if (!(this instanceof Style)) {
    return new Style(f);
  }
  this.__value = typeof f === "function" ? f : props => f;
}

Style.of however, should be constrained to always take a function props -> Styleand not rely on inspecting the contents of its arguments to do the right thing. Currently it passes through to the constructor, which inspects the guts. This means that Style.of({arbitraryObject})works, when it really should not. Whether it should be enforced by the system or not (i.e. type-check the argument to of) is an open question.

buzzdecafe avatar Mar 16 '17 12:03 buzzdecafe

Oops, merged #8 before I saw that - I'm confused as to why we can't have Style.of only accept and object and not a function that returns an object?

jem-computer avatar Mar 17 '17 02:03 jem-computer

of is crazy man. It's lawless (see https://www.schoolofhaskell.com/user/gbaz/building-up-to-a-point-via-adjunctions). But there's an intuition:

AnyTypeAtAll.of(anyValueAtAll).map(x => "I just mapped x")  // AnyTypeAtAll("i just mapped x")

So the question is what works with map?

DrBoolean avatar Mar 17 '17 02:03 DrBoolean

I am struggling with what the type of this thing is. Is Style a "container" for a function (Props) -> Style CSS? Or is Style the function itself? If it is the function itself, it makes sense to me that of could take arbitrary values and wrap them in a function, and Style is a type-alias for that function. If it is a "container" of a function (i.e. this._value is always a function), then it makes sense to me that of should always take a function.

so lemme take a whack at @drboolean 's formulation.

Case #1, Style is a container of a function

Style.of(g).map(f) I would expect Style(f . g)? Is that the intent?

Case #2, Style is the function

Style.of(x).map(f) I would expect Style(f x) this implies style may contain arbitrary values, i.e. this._value may not be a function. But it may!

buzzdecafe avatar Mar 17 '17 13:03 buzzdecafe

I am struggling with what the type of this thing is

Me too, Style seems something like this

type Style = (props: Props) => CSS

where Props and CSS are not better specified. Based on the examples in the README they are something like

type Dictionary = { [key: string]: any }
type Props = Dictionary
type CSS = Dictionary

So type Style = (props: Dictionary) => Dictionary.

Further implements FantasyLand 1, FantasyLand 2, FantasyLand 3 compatible Semigroup, Monoid, Functor, Apply, Applicative, Chain, ChainRec and Monad

Note that if Style is (props: Dictionary) => Dictionary then it has kind * and writing Style a doesn't make sense. Style can't be a Functor, Applicative, Monad.

It's not even a Semigroup since, based on the example in the README, concat has the wrong signature

concat :: Style -> Dictionary -> Style
// should be
concat :: Style -> Style -> Style

gcanti avatar Mar 22 '17 07:03 gcanti

i am still fighting some confusion.

of wraps its input in a function. This means that the type of Style is Style :: (Dict -> Dict), yes? So as Giulio says above, what does Style a mean? Where is the polymorphism if the sig is so specific?

This makes me wonder about the sigs for e.g. map and chain. If the type variables a and b do not have meaning what is Style a ~> (a -> b) -> Style b?

resolve is described as being Props -> CSS; and map is defined:

Style.prototype.map = function(f) {
  return new Style(props => {
    return f(this.resolve(props));   // resolve :: Props -> CSS
  });
};

... since resolve is Props -> CSS, and Style is a container of Props -> CSS, then f must be CSS -> CSS, yes? Are CSS and Props aliases?

buzzdecafe avatar Mar 24 '17 01:03 buzzdecafe

welp I missed a bunch of notifications, my bad @gcanti @buzzdecafe

let's presume that I messed up the types in the docs - the library works as I imagined, I just wrote the HM types wrong :)

you're right - Style is a container of a function that resolves to a dictionary of CSS Style (anyProps -> ResolvedCSS)

... since resolve is Props -> CSS, and Style is a container of Props -> CSS, then f must be CSS -> CSS, yes? Are CSS and Props aliases? yes.

// no knowledge of 'props', just manipulating a
// CSS object
// makeTheLogoBigger :: CSS -> CSS
const makeTheLogoBigger = inputCSS => ({
  ...inputCSS,
  width: inputCSS.width + 100,
  height: inputCSS.height + 100,
});

// let's really break this down for clarity
// logoFn :: props -> CSS
const logoFn = props => ({
  backgroundImage: `url(${props.url})`,
  height: 48, 
  width: 48,
  border: props.outlined ? '2px solid hotPink' : 'none',
});

// logo :: Style (props -> CSS)
const logo = Style(logoFn);

in my intuition,

const logoBigger = logo.map(css => makeTheLogoBigger(css))

would still return Style (props -> CSS)

which we could then resolve like this:

const result = logoBigger.resolve({
  url: 'placekitten.com/200',
  outlined: true
})
/* {
  backgroundImage: url(placekitten.com/200),
  height: 148,
  width: 148,
  border: '2px solid hotPink'
}
*/

So I think these are (some of) the docs that are wrong, that I'll fix once we figure out what they should be Style :: => (Props -> CSS) -> Style CSS

Style CSS is wrong here, my bad. I think it should be Style (Props -> CSS)

.of :: a -> Style a

this is wrong - with our current model it throws away the props it receives but it's still Style (Props -> CSS)

#concat :: Style a ~> Style a ~> Style a

should be Style (Props -> CSS) -> Style (Props -> CSS) -> Style (Props -> CSS)?

the example in the readme is wrong

Style.of({ fontWeight: 'bold', fontSize: 14 }).concat({ fontSize: 16, backgroundColor: 'red' })
// should be 
Style.of({ fontWeight: 'bold', fontSize: 14 }).concat(Style.of({ fontSize: 16, backgroundColor: 'red' }))
#map :: Style a ~> (a -> b) -> Style b

should be Style (props -> CSS) ~> (CSS -> CSS) -> Style CSS

#chain :: Style a ~> Style a -> Style a

this just looks incomplete - my bad - I think it should be Style (props -> CSS) ~> (CSS -> Style (props -> CSS)) -> Style CSS

Should I keep going / are we getting somewhere? My bad for messing up the docs; props to @jbaxleyiii for inferring what I meant not what I wrote :)

I'm still a little bit confused about the type I guess - is there a way to shorten Style (props -> CSS)? that was a lot of typing

jem-computer avatar Mar 26 '17 18:03 jem-computer

Thanks @jongold , that addresses a lot of my confusion when trying to grok the docs. I think @gcanti 's comments still need addressing, though, especially:

Note that if Style is (props: Dictionary) => Dictionary then it has kind * and writing Style a doesn't make sense. Style can't be a Functor, Applicative, Monad.

buzzdecafe avatar Mar 27 '17 15:03 buzzdecafe

Note that if Style is (props: Dictionary) => Dictionary then it has kind *

Re-reading the examples I think this is not true. For example

const makeTheLogoBigger = inputCSS => ({
  ...inputCSS,
  width: inputCSS.width + 100,
  height: inputCSS.height + 100,
});

inputCSS must be some subtype of { width: number, height: number } I guess. So the typings should be

export type CSSValue = number | string | ...
export type CSS = { [key: string]: CSSValue }
export type Props = { [key: string]: any }
export type Style<UB extends Props> = <P extends UB>(props: P) => CSS

So Style<A> is parametric.

From a theoretical point of view I think these typings can be justified considering another category (let's call it Sty) instead of JS, where JS is the usual category where

  • objects are all usual types of JavaScript
  • morphisms are all usual functions of JavaScript

Sty is (Props, Props -> Props) that is

  • objects are all the dictionaries
  • morphisms are all functions from a dictionary to a dictionary

Note that A in the definition of Style<A> is in contravariant position, so we can hope to find an instance of a contravariant (endo)functor, not a (endo)functor (just like React components).

A possible implementation in TypeScript

export type CSSValue = number | string
export type CSS = { [key: string]: CSSValue }
export type Props = { [key: string]: any }
export type Style<UB extends Props> = <P extends UB>(props: P) => CSS

export function contramap<A extends Props, B extends Props>(f: (b: B) => A, fa: Style<A>): Style<B> {
  return <P extends B>(b: P) => fa(f(b))
}

type A = {
  width: number,
  height: number
}

const makeBigger = <P extends A>(a: P) => Object.assign({}, a, {
  width: a.width + 100,
  height: a.height + 100
})

console.log(makeBigger({ width: 100, height: 50, color: 'red' })) // => { width: 200, height: 150, color: 'red' }

function chooseColorFromWidth(a: A): A & { color: string } {
  return Object.assign({}, a, {
    color: a.width < 100 ? 'red' : 'blue'
  })
}

const chooseColorAndMakeBigger = contramap(chooseColorFromWidth, makeBigger)

console.log(chooseColorAndMakeBigger({ width: 50, height: 100 })) // => { width: 150, height: 200, color: 'red' }
console.log(chooseColorAndMakeBigger({ width: 100, height: 100 })) // => { width: 200, height: 200, color: 'blue' }

Hope it helps

gcanti avatar Mar 31 '17 11:03 gcanti

wow, this is super helpful - thanks so much @gcanti.

jem-computer avatar Apr 01 '17 05:04 jem-computer