Skip to content
This repository has been archived by the owner on Feb 16, 2021. It is now read-only.

Add fantasy land support #55

Open
gcanti opened this issue Mar 11, 2017 · 10 comments
Open

Add fantasy land support #55

gcanti opened this issue Mar 11, 2017 · 10 comments

Comments

@gcanti
Copy link
Owner

gcanti commented Mar 11, 2017

Proof of concept (borrowed from fp-ts)

// @flow

interface HKT<F, A> {
  __hkt: F,
  __hkta: A
}

interface Functor<F> {
  map<A, B>(f: (a: A) => B, fa: HKT<F, A>): HKT<F, B>
}

type URI = 'Option';

type HKTOption<A> = HKT<URI, A>;

class None {
  __hkt: URI
  __hkta: any
  map<B>(f: (a: any) => B): Option<B> {
    return ((none: any): Option<B>)
  }
  inspect(): string {
    return this.toString()
  }
  toString() {
    return 'None'
  }
}

class Some<A> {
  __hkt: URI
  __hkta: A
  value: A
  constructor(value: A) {
    this.value = value
  }
  map<B>(f: (a: A) => B): Option<B> {
    return some(f(this.value))
  }
  inspect(): string {
    return this.toString()
  }
  toString() {
    return `Some(${JSON.stringify(this.value)})`
  }
}

type Option<A> = None | Some<A>;

const none = new None()

function some<A>(a: A): Option<A> {
  return new Some(a)
}

export function map<A, B>(f: (a: A) => B, fa: HKTOption<A>): Option<B> {
  return ((fa: any): Option<A>).map(f)
}

const double = (n: number): number => n * 2
const length = (s: string): number => s.length

function lift<F, A, B>(functor: Functor<F>, f: (a: A) => B): (fa: HKT<F, A>) => HKT<F, B> {
  return fa => functor.map(f, fa)
}

const maybeDouble = lift({ map }, double)

console.log(maybeDouble(some(10))) // Some(20)

console.log(some(1).map(double)) // Some(2)
console.log(none.map(double)) // None
// console.log(some(1).map(length)) // error: number. This type is incompatible with the expected param type of string
@alexeygolev
Copy link

I implemented Option and Either based on what you did in fp-ts and will gladly create a PR (both Option and Either are covered with tests). However I'm not sure what direction do you want to take with this. Will this implementation replace the one with inj/prj? Also as flowtype doesn't have module augmentation some of the things you do in fp-ts are note transferrable to flow. Do you have ideas on how it can possibly be done.
Another thing I was thinking about is making Option and Either a bit more accessible to the general js programmer. The amount of libraries that have their own Option/Maybe implementation is growing in a way similar to Promises libraries at some point in the past. Unfortunately the majority of them throw category theory in your face first thing in the Readme file. I don't think it actually encourages people to just grab and use it as it presents itself as a more complex matter than it is (for general use cases).
Take Scala's Option — without Cats/Scalaz it's a very simple datatype that is used by Scala's prelude functions. I guess for the most part It's more of a documentation issue which I'll be glad to help with if you feel like it can be useful.

Another thing — a bit of a blasphemy for pure fantasy land folks — can we think of a method similar to Promise.then which can combine map and flatMap/chain for convenience? We still keep map and chain but by making the API similar to Promises we might push Option a bit closer to be standardised across the libraries and possibly in the language itself. I'm sure people would rather have an Either being returned by JSON.parse rather then wrapping it into a try/catch.

@gcanti
Copy link
Owner Author

gcanti commented Mar 29, 2017

Will this implementation replace the one with inj/prj?

If we go the fantasy-land route then yes inj/prj are replaced by new / this.value (when possible, for example the module Arr, if we don't want to wrap the native arrays, should keep the inj/prj pair I guess )

Also as flowtype doesn't have module augmentation some of the things you do in fp-ts are note transferrable to flow

We can define overloadings though it's not the same (augmentations can be distributed while overloadings are centralised)

Example with lift and Option

// file Functor.js
import type { Option, URI as OptionURI } from './Option'

export interface Functor<F> {
  map<A, B>(f: (a: A) => B, fa: HKT<F, A>): HKT<F, B>
}

// more overloadings here...
declare function lift<A, B>(functor: Functor<OptionURI>, f: (a: A) => B): (fa: Option<A>) => Option<B>
export function lift<F, A, B>(functor: Functor<F>, f: (a: A) => B): (fa: HKT<F, A>) => HKT<F, B> {
  return fa => functor.map(f, fa)
}

can we think of a method similar to Promise.then which can combine map and flatMap/chain for convenience?

Not sure what you gain from merging map and chain though

@gcanti
Copy link
Owner Author

gcanti commented Mar 29, 2017

Will this implementation replace the one with inj/prj?

Actually we could also implement a fromNullable / toNullable pair

export function fromNullable<A>(a: ?A): Option<A> {
  return a == null ? none : some(a)
}

export function toNullable<A>(fa: Option<A>): ?A {
  return fa instanceof Some ? fa.value : null
}

and then alias them to inj / prj to keep the old (deprecated) API unchanged


[DISCLAIMER] I switched to TypeScript and I think I won't look back anytime soon, so I'll dedicate much less time to flow-static-land

@ctrlplusb
Copy link

@gcanti do you have any write ups anywhere describing why you made the switch? I thought that the type variance support of flow would play nicer with some advanced FP concepts, but perhaps I am misguided. :)

@gcanti
Copy link
Owner Author

gcanti commented Apr 5, 2017

@ctrlplusb No, I only have a list of pros and cons (presented in an internal meeting at buildo) half in english and half in italian.

The gist is that TypeScript is a great tool from a pragmatic point of view. Also indexed types are awesome.

the type variance support of flow would play nicer with some advanced FP concepts

Indeed, that and its advanced type inference are what I miss the most, especially in the context of functional programming.

@mwalkerwells
Copy link
Contributor

Is this something you're still considering?

While I know you don't prefer point-free programming, I'd love to see this! @DrBoolean

export const map = <A, B>(f: A => B): (HKTOption<A> => Option<B>) => fa => {
  return ((fa: any): Option<A>).map(f)
}

const foo = compose(
  map(log),
  map(double),
  some,
)

foo(10) // Some(20)

@mwalkerwells
Copy link
Contributor

Seems like this should error?

console.log(maybeDouble(some(''))) // No Error

@mwalkerwells
Copy link
Contributor

This fixes the above error...

class None {
  __hkt: URI
  __hkta: any
// ...
}

to...

class None<A> {
  __hkt: URI
  __hkta: A
// ...
}

The any breaks the error checking here...

@mwalkerwells
Copy link
Contributor

mwalkerwells commented May 28, 2017

With the motivation of making things more clear & approachable, I'm curious what you all think of the following changes:

  1. Preference of static applicative methods (Nothing.of()) instead of const none = new None() & function some<A>(a: A): Option<A> { return new Some(a) }
  2. Type name simplification: Kind instead of HKT
  3. A single __type instance property to denote Kind
  4. More explicit referenes instead of URI
  5. While a bit duplicative, expressive use of implements to show relationship to Kind.
// @flow

interface Kind<F, A> {
  __type: [ F, A ]
}

interface Functor<F> {
  map<A, B>(f: A => B, fa: Kind<F, A>): Kind<F, B>
}

type Maybe<A> = Nothing<A> | Just<A>

class Nothing<A> implements Kind<'Maybe', A> {
  __type: [ 'Maybe', A ]
  static of(): Maybe<A> {
    return new Nothing()
  }
  map<B>(f: A => B): Maybe<B> {
    return (Nothing.of(): any)
  }
  inspect(): string {
    return this.toString()
  }
  toString(): string {
    return 'Nothing'
  }
}

class Just<A> implements Kind<'Maybe', A> {
  __type: [ 'Maybe', A ]
  value: A
  static of(value: A): Maybe<A> {
    return new Just(value)
  }
  constructor(value: A) {
    this.value = value
  }
  map<B>(f: A => B): Maybe<B> {
    return Just.of(f(this.value))
  }
  inspect(): string {
    return this.toString()
  }
  toString(): string {
    return `Just(${JSON.stringify(this.value)})`
  }
}

export function map<A, B>(f: A => B, fa: Kind<'Maybe', A>): Maybe<B> {
  return ((fa: any): Maybe<A>).map(f)
}

const double = (n: number): number => n * 2
const length = (s: string): number => s.length

function lift<F, A, B>(functor: Functor<F>, f: A => B): Kind<F, A> => Kind<F, B> {
  return fa => functor.map(f, fa)
}

const maybeDouble = lift({ map }, double)

console.log(maybeDouble(Just.of(10))) // Just(20)

console.log(Just.of(1).map(double)) // Just(2)
console.log(Nothing.of().map(double)) // Nothing
console.log(Just.of(1).map(length)) // error: number. This type is incompatible with the expected param type of string

@zerobias
Copy link

zerobias commented Aug 2, 2017

Note that flow now has opaque types, which can help to declare type relations
https://medium.com/flow-type/hiding-implementation-details-with-flows-new-opaque-type-aliases-feature-40e188c2a3f9

$Values and $ElementType can help too

Also, if you use comment syntax, you can avoid unnecessarily real code - no if (false) { no more

image

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

5 participants