Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Start working on v2 #823

Closed
gcanti opened this issue Apr 12, 2019 · 191 comments
Closed

Start working on v2 #823

gcanti opened this issue Apr 12, 2019 · 191 comments
Milestone

Comments

@gcanti
Copy link
Owner

gcanti commented Apr 12, 2019

@sledorze @raveclassic @grossbart

I was waiting for microsoft/TypeScript#26349 in order to start working on fp-ts@2.x but alas looks like it will be delayed for an indefinite period.

The current codebase has accumulated some cruft during the last few months, which is a good thing because fp-ts is now way better than say a year ago, but is also much confusing for new comers.

I think it's time to clean up the codebase and release a polished fp-ts@2.x version:

  • remove all the deprecated APIs / modules
  • remove all phantom fields
  • other changes? (please help me with this)

What do you think?

@gcanti gcanti added this to the 2.0 milestone Apr 12, 2019
@sledorze
Copy link
Collaborator

@gcanti
We may also reconsider/refresh/restate on that one:
#543

@raveclassic
Copy link
Collaborator

@gcanti Awesome!

Sidenote - should we wait for microsoft/TypeScript#30790 and try to implement generator-based do-notation once again and add it to the core if succeeded?

@sledorze
Copy link
Collaborator

@raveclassic is that performant? (sorry not checked by thinking that is not the case)

@sledorze
Copy link
Collaborator

Some pain points I've seen so far around the HKT encoding (without any proposition):

  • People have a hard time when somewhere they broke a module augmentation and don't have a clue as where the breaking comes from.
  • People don't know where to put their instances; Haskell/Purescript drives you on that matter.

Maybe we can find some solutions to those, if the solutions result in a breaking change that may be the right time to discuss it

@raveclassic
Copy link
Collaborator

@sledorze I haven't checked performance. Still I think ability to write do-notation-like code is a greater benefit anyway. However current implementation is stack-unsafe.

@grossbart
Copy link
Sponsor Collaborator

I like what was listed already 👍 Here's some more (completely random) ideas:

  • My team was very confused with the 2v modules (“what is this and why is it there?”), so if these will replace the deprecated modules that is a good thing for version 2, I think. (I personally do like this approach to prevent breaking existing code, but I understand the confusion, too.)
  • Maybe we could also rethink some harder to use APIs. An example I have here is liftA<n> where the applied function is curried and has to be manually typed; the way I understand this a simplification along the lines of liftA2(option)((a, b) => {}, optA, optB) could make type inference for the arguments a and b work. (I could be mistaken and it might be that I just couldn't figure out how to make type inference work 😄.)
  • Functions like groupBy could return a Map (see also this issue: Record keys should be string | number | symbol instead of only string #730, for example).
  • Figure out how to best work with ReadonlyArrays Array.last: add support for ReadonlyArray #544, after all FP is all about not mutating things.
  • Maybe a similar issue arises for tuples, now that they can be made readonly in TS3.4?
  • This would be a good time to replace the deprecated inspect functions for Node with the new way to do it Node 10 DeprecationWarning #408

@gcanti
Copy link
Owner Author

gcanti commented Apr 14, 2019

@grossbart RE: liftA<n>. Now that we have sequenceT / sequenceS I think we can deprecate the liftA<n> functions.

Moreover they don't play well with polymorphic functions

import { liftA2 } from 'fp-ts/lib/Apply'
import { option } from 'fp-ts/lib/Option'

const toObj = <A>(a: A) => <B>(b: B): { a: A; b: B } => ({ a, b })

const toObjLifted = liftA2(option)(toObj)
/*
Note the inferred `{}`

const toObjLifted: <A>(a: Option<A>) => (b: Option<{}>) => Option<{
    a: A;
    b: {};
}>
*/

I'll send a PR (for v1.17)

liftA2(option)((a, b) => {}, optA, optB) usually is named map2, see #322

@sledorze
Copy link
Collaborator

Another thing I've seen is unlawful implementation of instances.
It may be useful to find a way to enforce lawfulness (or at least ease it / make it the default).

@gcanti
Copy link
Owner Author

gcanti commented Apr 15, 2019

RE: inspect / toString. I propose to skip over the issue by introducing the Show type class (I'll send a PR)

@grossbart
Copy link
Sponsor Collaborator

Thanks for the liftA<n> explanation: makes a lot of sense! I will try to think of a way to make this findable in the docs – for example, some on our team have struggled with finding Maybe, not knowing that some languages use Option for this concept, and this is a similar kind of problem: people will try to find what they know 😄 I didn't know about map<n>, either.

And I'm completely on board with the Show typeclass 👍

@gcanti
Copy link
Owner Author

gcanti commented Apr 17, 2019

Do we also want to switch to "naked" data types?

A glaring example: Writer.

There's no much of a benefit of using a class (only map as a method)

export class Writer<W, A> {
  constructor(readonly run: () => [A, W]) {}
  map<B>(f: (a: A) => B): Writer<W, B> {
    return new Writer(() => {
      const [a, w] = this.run()
      return [f(a), w]
    })
  }
}

We could switch to a unwrapped representation

export interface Writer<W, A> {
  (): [A, W]
}

We could also declassify

  • Identity
  • Pair
  • Tuple

and maybe also

  • State
  • Reader
  • ReaderTaskEither

@sledorze
Copy link
Collaborator

Note: One of the reasons for preferring instantiating a class over a POJO ( :) ), is the cost of construction (depending on the number of methods/functions).

@gcanti
Copy link
Owner Author

gcanti commented Apr 17, 2019

@sledorze I mean just the function (no POJO)

Here's the Writer module as an example

import { phantom } from './function'
import { Functor2 } from './Functor'
import { Monad2C } from './Monad'
import { Monoid } from './Monoid'

declare module './HKT' {
  interface URI2HKT2<L, A> {
    Writer: Writer<L, A>
  }
}

export const URI = 'Writer'

export type URI = typeof URI

/**
 * @since 1.0.0
 */
export interface Writer<W, A> {
  (): [A, W]
}

export const evalWriter = <W, A>(fa: Writer<W, A>): A => {
  return fa()[0]
}

export const execWriter = <W, A>(fa: Writer<W, A>): W => {
  return fa()[1]
}

const map = <W, A, B>(fa: Writer<W, A>, f: (a: A) => B): Writer<W, B> => {
  return () => {
    const [a, w] = fa()
    return [f(a), w]
  }
}

/**
 * Appends a value to the accumulator
 *
 * @since 1.0.0
 */
export const tell = <W>(w: W): Writer<W, void> => {
  return () => [undefined, w]
}

/**
 * Modifies the result to include the changes to the accumulator
 *
 * @since 1.3.0
 */
export const listen = <W, A>(fa: Writer<W, A>): Writer<W, [A, W]> => {
  return () => {
    const [a, w] = fa()
    return [[a, w], w]
  }
}

/**
 * Applies the returned function to the accumulator
 *
 * @since 1.3.0
 */
export const pass = <W, A>(fa: Writer<W, [A, (w: W) => W]>): Writer<W, A> => {
  return () => {
    const [[a, f], w] = fa()
    return [a, f(w)]
  }
}

/**
 * Projects a value from modifications made to the accumulator during an action
 *
 * @since 1.3.0
 */
export const listens = <W, A, B>(fa: Writer<W, A>, f: (w: W) => B): Writer<W, [A, B]> => {
  return () => {
    const [a, w] = fa()
    return [[a, f(w)], w]
  }
}

/**
 * Modify the final accumulator value by applying a function
 *
 * @since 1.3.0
 */
export const censor = <W, A>(fa: Writer<W, A>, f: (w: W) => W): Writer<W, A> => {
  return () => {
    const [a, w] = fa()
    return [a, f(w)]
  }
}

/**
 *
 * @since 1.0.0
 */
export const getMonad = <W>(M: Monoid<W>): Monad2C<URI, W> => {
  const of = <A>(a: A): Writer<W, A> => {
    return () => [a, M.empty]
  }

  const ap = <A, B>(fab: Writer<W, (a: A) => B>, fa: Writer<W, A>): Writer<W, B> => {
    return () => {
      const [f, w1] = fab()
      const [a, w2] = fa()
      return [f(a), M.concat(w1, w2)]
    }
  }

  const chain = <A, B>(fa: Writer<W, A>, f: (a: A) => Writer<W, B>): Writer<W, B> => {
    return () => {
      const [a, w1] = fa()
      const [b, w2] = f(a)()
      return [b, M.concat(w1, w2)]
    }
  }

  return {
    URI,
    _L: phantom,
    map,
    of,
    ap,
    chain
  }
}

/**
 * @since 1.0.0
 */
export const writer: Functor2<URI> = {
  URI,
  map
}

@cruhl
Copy link

cruhl commented Apr 17, 2019

I've gotten a ton of mileage out of ts-do and would love to see do notation emerge as a priority. It's been really useful in selling FP-TS across various teams where I work.

@gcanti
Copy link
Owner Author

gcanti commented Apr 17, 2019

@cruhl IIRC ts-do works by patching the prototype, fp-ts-contrib's Do looks more lightweight and flexible (works with any Monad)

@joshburgess
Copy link
Contributor

What do you think about renaming Setoid to Eq for 2.0? I think you mentioned this before, but not sure.

@sledorze
Copy link
Collaborator

I want to share that I m a bit worried about discoverability with the new static land approach.
It won t ease adoption.
What s the main driver? Tree shaking?

@gcanti
Copy link
Owner Author

gcanti commented Apr 24, 2019

@sledorze IMO the main drivers are the following:

  1. hacky sum type encoding

Example

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

instead of

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

and

type Either<L, A> = Left<L, A> | Right<L, A>

instead of

type Either<L, A> = Left<L> | Right<A>
  1. not general

Some data type needs something (provided by the user) in order to implement a particular instance (e.g. Validation, Writer, etc...).
For such data types there's no fluent APIs nor a solution at the moment.

Also fluent APIs, even when we can define them, are not enough: see the desire for something like Do in fp-ts-config

  1. serialization / deserialization

Serialization / deserialization is just more complicated than necessary.

  1. tree shaking

AFAIC classes are a barrier (I'm not an expert though).


However fluent APIs are nice!

fromNullable(foo)
  .chain(x => bar)
  .map(baz)

versus

map(chain(fromNullable(foo), x => bar), baz)

So fp-ts users / hackers, I need your help to find a general solution.

@mlegenhausen
Copy link
Collaborator

Currying for the rescue?

pipe(
  chain(x => bar),
  map(baz)
)(formNullable(foo))

Or maybe we could create something like

flow(option).chain(x => bar).map(baz).exec(fromNullable(foo))

// or

flow(option)(fromNullable(foo)).chain(x => bar).map(baz)

@sledorze
Copy link
Collaborator

sledorze commented Apr 24, 2019

@mlegenhausen I really like what you're proposing as a fluent API enabler pattern.
This is orthogonal.
I think I can see how it would be achieved generically via mapped conditionals and I think it could also be possible to tackle userland extension usages that way (but this would includes some limitations when in the 'fluent' form).
Do you already have implemented an encoding?

@sledorze
Copy link
Collaborator

@gcanti thanks for sharing the drivers; that will ease a lot of work (besides the loss of the fluent API).

@sledorze
Copy link
Collaborator

my bad, wrong button..

@sledorze sledorze reopened this Apr 24, 2019
@gcanti
Copy link
Owner Author

gcanti commented Apr 24, 2019

@mlegenhausen @sledorze this is a POC based on an idea by @mattiamanzati

Some observations:

  1. Fluent is able to autoconfigure itself (at the type-level) based on the passed instance.

So for example in the snippet below if we pass writer (from import { writer } from 'fp-ts/lib/Writer and which is only a Functor) instead of getMonad(monoidString) the method chain is not available (and it doesn't even show up in VS Code).

  1. this POC supports Functor and Monad but we can add support for other type classes (Setoid, Semigroup, Alternative, Foldable, etc...)

  2. we could also add to Fluent some functions like flatten (which could be removed from the Chain module), apFirst, apSecond, chainFirst, chainSecond, etc...

import { URIS, Type, HKT, Type2, URIS2 } from 'fp-ts/lib/HKT'
import { Functor1, Functor, Functor2, Functor2C } from 'fp-ts/lib/Functor'
import { Chain1, Chain, Chain2, Chain2C } from 'fp-ts/lib/Chain'

export interface Fluent2C<F extends URIS2, I, L, A> {
  readonly I: I
  readonly value: Type2<F, L, A>
  map<B>(this: Fluent2C<F, Functor2C<F, L>, L, A>, f: (a: A) => B): Fluent2C<F, I, L, B>
  chain<B>(
    this: Fluent2C<F, Chain2C<F, L>, L, A>,
    f: (a: A) => Type2<F, L, B> | Fluent2C<F, I, L, B>
  ): Fluent2C<F, I, L, B>
}

export interface Fluent2<F extends URIS2, I, L, A> {
  readonly I: I
  readonly value: Type2<F, L, A>
  map<B>(this: Fluent2<F, Functor2<F>, L, A>, f: (a: A) => B): Fluent2<F, I, L, B>
  chain<B>(this: Fluent2<F, Chain2<F>, L, A>, f: (a: A) => Type2<F, L, B> | Fluent2<F, I, L, B>): Fluent2<F, I, L, B>
}

export interface Fluent1<F extends URIS, I, A> {
  readonly I: I
  readonly value: Type<F, A>
  map<B>(this: Fluent1<F, Functor1<F>, A>, f: (a: A) => B): Fluent1<F, I, B>
  chain<B>(this: Fluent1<F, Chain1<F>, A>, f: (a: A) => Type<F, B> | Fluent1<F, I, B>): Fluent1<F, I, B>
}

const normalize = <F, A>(fa: HKT<F, A> | Fluent<F, unknown, A>): HKT<F, A> => (fa instanceof Fluent ? fa.value : fa)

export class Fluent<F, I, A> {
  constructor(readonly I: I, readonly value: HKT<F, A>) {}
  map<I extends Functor<F>, B>(this: Fluent<F, I, A>, f: (a: A) => B): Fluent<F, I, B> {
    return new Fluent<F, I, B>(this.I, this.I.map(this.value, f))
  }
  chain<I extends Chain<F>, B>(this: Fluent<F, I, A>, f: (a: A) => HKT<F, B> | Fluent<F, I, B>): Fluent<F, I, B> {
    return new Fluent<F, I, B>(this.I, this.I.chain(this.value, a => normalize(f(a))))
  }
}

export function fluent<F extends URIS2, I, L>(I: { URI: F; _L: L } & I): <A>(fa: Type2<F, L, A>) => Fluent2C<F, I, L, A>
export function fluent<F extends URIS2, I>(I: { URI: F } & I): <L, A>(fa: Type2<F, L, A>) => Fluent2<F, I, L, A>
export function fluent<F extends URIS, I>(I: { URI: F } & I): <A>(fa: Type<F, A>) => Fluent1<F, I, A>
export function fluent<F, I>(I: { URI: F } & I): <A>(fa: HKT<F, A>) => Fluent<F, I, A>
export function fluent<F, I>(I: { URI: F } & I): <A>(fa: HKT<F, A>) => any {
  return fa => new Fluent(I, fa)
}

//
// Usage
//

// Option

import { option, some, none } from 'fp-ts/lib/Option'

const fluentO = fluent(option)

const x = fluentO(some(42))
  .map(n => n * 2)
  .chain(n => (n > 2 ? some(n) : none)).value
console.log(x) // some(84)

// Writer

import { getMonad, Writer } from 'fp-ts/lib/Writer'
import { monoidString } from 'fp-ts/lib/Monoid'

const fluentW = fluent(getMonad(monoidString))

const y = fluentW(new Writer(() => [1, 'a']))
  .map((n: number): number => n + 2)
  .chain(n => new Writer(() => [n + 1, 'b'])).value
console.log(y.run()) // [ 4, 'ab' ]

// Either

import { either, right } from 'fp-ts/lib/Either'

const fluentE = fluent(either)

const z = fluentE(right<string, number>(1)).map(n => n + 1).value
console.log(z) // right(2)

@gcanti
Copy link
Owner Author

gcanti commented Apr 24, 2019

What do you think about renaming Setoid to Eq for 2.0?

@joshburgess I'm not sure it's worth it but I'm not against this change (IMO Eq is nicer than Setoid).

People please vote with a 👍 or a 👎: should we rename Setoid to Eq?

@sledorze
Copy link
Collaborator

sledorze commented Apr 24, 2019

Can we keep a compatibility layer? (type Alias)

@sledorze
Copy link
Collaborator

sledorze commented Apr 24, 2019

@gcanti About the Fluent approach, it will also have a runtime impact; I wonder what would be the stance on patching the prototype (can't believe I say that).

But really what I'm not so fan of is the fact one will have to importing modules or be very precise on aliasing specific functions in order to prevent collisions.

So this code

pipe(
  chain(x => bar),
  map(baz)
)(formNullable(foo))

in RWC will be more like:

pipe(
  O.chain(x => bar),
  O.map(baz)
)(O.formNullable(foo))

Which also adds an indirection.

There's solutions to that (alias function names prefix defined in the module).

I'm not saying that its good or bad, but we have to be conscious of the tradeoffs.
(and once chosen, think about the DX for migrating code bases)

@mlegenhausen
Copy link
Collaborator

mlegenhausen commented Jun 20, 2019

@gcanti thanks for fp-ts@2 I have the pleasure to use it in a new project. From my perspective the DX is better even when I have to write a little more code due the pipe operator but the combination with rxjs is much more uniform.

Some "aha" moments I really liked

  • async (): A => is a Task<A> and async (): Either<L, R> => is a TaskEither<L, R>
  • I can combine Task with TaskEither operators. No need anymore for foldTask from the old TaskEither simply use fold from Task. I think the same is true for IO and IOEither
  • All the simplifications for defining a Reader or IO. This makes understanding much more easier.

Just a question or feature request. But is it possible to make traverse pipeable?

@gcanti
Copy link
Owner Author

gcanti commented Jun 20, 2019

@YBogomolov I'm not a fan of default type parameters but what about

export function left<E = never, A = never>(e: E): Either<E, A> {
  return { _tag: 'Left', left: e }
}

export function right<E = never, A = never>(a: A): Either<E, A> {
  return { _tag: 'Right', right: a }
}

Result

import * as E from '../src/Either'
import { pipe } from '../src/pipeable'
import * as O from '../src/Option'

const ea = E.left<string, number>('e')
const eb = E.right<string, number>(42)
const ec = pipe(
  ea,
  E.alt(() => eb) // ok
)

const ed = pipe(
  eb,
  E.alt(() => ea) // ok
)

const a: E.Either<string, number> = pipe(
  O.some(42), 
  O.fold(() => E.left('it was none'), E.right) // ok
)

const res = (cond: boolean) => (cond ? E.left(42) : E.right('what?'))
const v = res(true)
if (E.isRight(v)) {
  const x = v.right // string ok
} else {
  const x = v.left // number ok
}

I get only 2 errors in test/Either.ts and both are about ap

assert.deepStrictEqual(_.either.ap(_.left('mabError'), _.right('abc')), _.left('mabError')) // Argument of type 'Either<string, (a: never) => never>' is not assignable to parameter of type 'Either<string, (a: string) => unknown>'
const M = _.getValidation(monoidString)
assert.deepStrictEqual(M.ap(_.left('foo'), _.right(1)), _.left('foo')) // Argument of type 'Either<string, (a: never) => never>' is not assignable to parameter of type 'Either<string, (a: number) => unknown>'

@mlegenhausen yeah, it's awesome (also Reader / ReaderEither)

is it possible to make traverse pipeable?

Let me try... stay tuned.

@YBogomolov
Copy link
Contributor

@gcanti That seems to be really awesome!

@gcanti
Copy link
Owner Author

gcanti commented Jun 20, 2019

@YBogomolov Also it's more "backward compatible".

I guess that it must by applied to the other constructors / lifting functions too

// TaskEither.ts

export const left: <E = never, A = never>(e: E) => TaskEither<E, A> = T.left

export const right: <E = never, A = never>(a: A) => TaskEither<E, A> = T.of

export function rightIO<E = never, A = never>(ma: IO<A>): TaskEither<E, A> {
  return rightTask(task.fromIO(ma))
}

export function leftIO<E = never, A = never>(me: IO<E>): TaskEither<E, A> {
  return leftTask(task.fromIO(me))
}

// etc...

Result

import * as TE from '../src/TaskEither'

const b = pipe(
  O.some(42),
  O.fold(() => TE.left('it was none'), a => TE.right(a)) // ok
)

const c = pipe(
  O.some(42),
  O.fold(() => TE.leftIO(() => 'it was none'), a => TE.rightIO(() => a)) // ok
)

@gcanti
Copy link
Owner Author

gcanti commented Jun 20, 2019

@mlegenhausen ok it's possible but it would add a lot of code to the pipeable module, I'm not sure it's worth it.

@YBogomolov released fp-ts@2.0.0-rc.7 with the discussed changes, let me know how it goes.

Also I've published the following versions which are compatible with fp-ts@2.0.0-rc.6+

  • io-ts@1.10.0
  • fp-ts-contrib@0.1.0
  • monocle@2.0.0-rc.1
  • newtype-ts@0.3.0

@YBogomolov
Copy link
Contributor

@gcanti Giulio, thank you! 🙏🏻 In my opinion, such default parameters add a lot to development convenience. I've updated kleisli-ts to 2.0.0-rc.7, removed explicit parameters in fold invocation, and everything is working fine: https://travis-ci.org/YBogomolov/kleisli-ts/builds/548291849

@sledorze
Copy link
Collaborator

sledorze commented Jun 24, 2019

To ease without import collision, it would be very handy to NOT have both the module reexport and Type classe implementation having the same name (e.g.: ord).
Maybe using a plurial form, like ords for reexporting modules could be a solution.
What do you think?

@mlegenhausen
Copy link
Collaborator

mlegenhausen commented Jun 26, 2019

@gcanti is there a reason why Monoidal was removed?

@gcanti
Copy link
Owner Author

gcanti commented Jun 26, 2019

@mlegenhausen mult looks useless since we have sequenceT

export interface Monoidal<F> extends Functor<F> {
  readonly unit: () => HKT<F, void>
  readonly mult: <A, B>(fa: HKT<F, A>, fb: HKT<F, B>) => HKT<F, [A, B]>
}

@rzeigler
Copy link
Contributor

Are there any additional issues before 2.0 release?

@gcanti
Copy link
Owner Author

gcanti commented Jun 30, 2019

Personally I'm satisfied by the status quo, and I'm ready to release 2.0 #892

@gcanti
Copy link
Owner Author

gcanti commented Jul 1, 2019

Thanks to all

the last few months have been pretty stressful for me (pushing fp-ts in a new direction and with so many breaking changes is a huge responsability), honestly I didn't expect so much work to do for 2.0 (80 days and counting of almost full time work) but I really do think that the result is great and I could not have done it without your help

@cdimitroulas
Copy link
Contributor

Thanks for all your hard work @gcanti - you're the real OG 🙏 🙌

@kightlingerh
Copy link

Thank you @gcanti - I haven't contributed much here but I love this library and I am a big fan of all the changes to v2. You did a great job!

@gcanti
Copy link
Owner Author

gcanti commented Jul 1, 2019

https://github.com/gcanti/fp-ts/releases/tag/2.0.0

@gcanti gcanti closed this as completed Jul 1, 2019
@gcanti gcanti unpinned this issue Jul 1, 2019
@gcanti gcanti removed the discussion label Jul 2, 2019
@chrisber
Copy link

chrisber commented Jul 2, 2019

Thanks for all your work, @gcanti the implementations are now a lot easier to follow. This helps a lot learning the FP paradigma. 👏 👏 👏

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

No branches or pull requests