gcanti / fp-ts

Functional programming in TypeScript
https://gcanti.github.io/fp-ts/
MIT License
10.88k stars 504 forks source link

Start working on v2 #823

Closed gcanti closed 5 years ago

gcanti commented 5 years ago

@sledorze @raveclassic @grossbart

I was waiting for https://github.com/Microsoft/TypeScript/pull/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:

What do you think?

sledorze commented 5 years ago

@gcanti We may also reconsider/refresh/restate on that one: https://github.com/gcanti/fp-ts/issues/543

raveclassic commented 5 years ago

@gcanti Awesome!

Sidenote - should we wait for https://github.com/Microsoft/TypeScript/pull/30790 and try to implement generator-based do-notation once again and add it to the core if succeeded?

sledorze commented 5 years ago

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

sledorze commented 5 years ago

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

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 commented 5 years ago

@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 commented 5 years ago

I like what was listed already πŸ‘ Here's some more (completely random) ideas:

gcanti commented 5 years ago

@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 https://github.com/gcanti/fp-ts/issues/322

sledorze commented 5 years ago

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 commented 5 years ago

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

grossbart commented 5 years ago

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 commented 5 years ago

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

and maybe also

sledorze commented 5 years ago

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 commented 5 years ago

@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 commented 5 years ago

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 commented 5 years ago

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

joshburgess commented 5 years ago

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

sledorze commented 5 years ago

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 commented 5 years ago

@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>

2) 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

3) serialization / deserialization

Serialization / deserialization is just more complicated than necessary.

4) 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 commented 5 years ago

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 commented 5 years ago

@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 commented 5 years ago

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

sledorze commented 5 years ago

my bad, wrong button..

gcanti commented 5 years ago

@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).

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

3) 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 commented 5 years ago

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 :+1: or a :-1:: should we rename Setoid to Eq?

sledorze commented 5 years ago

Can we keep a compatibility layer? (type Alias)

sledorze commented 5 years ago

@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)

gcanti commented 5 years ago

@sleodorze pipe looks less ergonomic though

const lift = <A, B>(f: (a: A) => B) => (fa: Option<A>): Option<B> => option.map(fa, f)

const flatMap = <A, B>(f: (a: A) => Option<B>) => (fa: Option<A>): Option<B> =>
  option.chain(fa, f)

// methods
const x1 = fromNullable(1)
  // no type annotation
  .chain(n => (n === 0 ? none : some(1 / n)))
  .map(n => n * 2)

// fluent
const x2 = fluentO(fromNullable(1))
  // no type annotation
  .chain(n => (n === 0 ? none : some(1 / n)))
  .map(n => n * 2).value

// with pipe
const x3 = pipe(
  // type annotation required
  flatMap((n: number) => (n === 0 ? none : some(1 / n))),
  lift(n => n * 2)
)(fromNullable(1)) // argument comes last
sledorze commented 5 years ago

@gcanti Indeed, however, not all API functions are covered by the proposed fluent solution. I wanted to exhibit the import constraints.

mattiamanzati commented 5 years ago

I think that pipe and pipeK should be implemented indeed, but in some cases fluent/methods is great because the order of appearance of the things is the same of the order of execution. With pipe you kinda "let me deal with this later" and readability may suffer from this :/

grossbart commented 5 years ago

I'm a bit sceptical of the removal of classes because the current fluent API works so smoothly and effortlessly. If we can find a good alternative, I'm all for it, of course, and some examples already point out the possibilities.

One reason for my scepticism stems from a project where we tried the Static Land approach over a Fantasy Land one and it led to very nested and verbose view code and lots of imports. Certainly a matter of taste, practice, and editor support, but a bit of a hurdle none the less.

Another reason arises when we consider this pseudo-code of a React view from a recent project:

const ShowInput = <A>(props: {a: Option<A>, b: Option<A>) => (
  <div>
    <p>{a.alt(b).map(x => `Input is ${x}`).getOrElse('No input given')}</p>
    {b.map(() => <p>[Alternative input]</p>).toNullable()}
  </div>
)

With the fluent() approach I would first have to create the correct instance function and then apply it to both a and b, no? Then: would alt or other combinators be available and when? Also, I'm not sure about getting the value out with fluent(…).value, wouldn't that be a bit confusing: myOpt.isRight() ? fluentO(myOpt).map(…).value.value : ''? I've seen people use this because they didn't know about fold et al and their editor suggested .value. I'm not sure this pattern should be encouraged.

In a way a fluent API is the JS way of emulating infix operators, so I went looking for some ideas for how to implement those. Can't be done, of course, but for the curious, I found two ideas: 1) and 2). Not actionable, but maybe leads to other ideas πŸ€·β€β™‚οΈ.

Some of the benefits like serializability are certainly worth something and maybe the fluent() approach works well, so maybe my scepticism is unwarranted πŸ˜„

mattgrande commented 5 years ago

@joshburgess - What is the motivation behind changing Setoid to Eq? I learned most of this language from Fantasyland, and they use Setoid.

(Not opposed to the change, just curious)

gcanti commented 5 years ago

@grossbart this is a direct comparison

(methods)

function ShowInput<A>(props: { a: Option<A>; b: Option<A> }) {
  return (
    <div>
      <p>
        {props.a
          .alt(props.b)
          .map(x => `Input is ${x}`)
          .getOrElse('No input given')}
      </p>
      {props.b.map(() => <p>['Alternative input']</p>).toNullable()}
    </div>
  )
}

(fluent)

function ShowInputFluent<A>(props: { a: Option<A>; b: Option<A> }) {
  return (
    <div>
      <p>
        {getOrElse(
          fluent(props.a)
            .alt(props.b)
            .map(x => `Input is ${x}`).value,
          'No input given'
        )}
      </p>
      {toNullable(fluent(props.b).map(() => <p>['Alternative input']</p>).value)}
    </div>
  )
}

would alt or other combinators be available and when?

We can manage many type classes, see this implementation.

Basically if you pass a suitable instance you can use any of the FluentHKT / FluentHKT2 methods.

Actually such a Fluent wrapper is useful even today for some data types like Validation, Writer, etc... so maybe we could release Fluent as experimental feature in fp-ts-contrib and try it out in RWC.

Also keep in mind that this is just a first idea (thanks @mattiamanzati !), we could come up with something better in the future.

their editor suggested .value. I'm not sure this pattern should be encouraged

This won't happen anymore in v2 as Left and Right store the value in different props

sledorze commented 5 years ago

@gcanti @grossbart I share the scepticism. I'm worried as much for the people out there which have code base to maintain (this is a short term issue) but also for newcomers, as it adds up to the difficulty to use the lib.

The fluent idea is very neat and the proposed implementation is clever, clean and to the point.

However, as the Fluent interface usage adds some runtime overhead, people will choose between speed and ease of writing, code base styles will become heterogeneous and more verbose, adding to the noise. I'm afraid the DX experience will just become bad.

joshburgess commented 5 years ago

@mattgrande Ah, I see. I actually didn't know that that's where the Setoid terminology came from. I think I've seen it somewhere else too... maybe Agda or Idris? I forget.

Anyway, it's not a big deal either way, but I kinda feel like the term unnecessarily scares away newcomers. Both Haskell & PureScript just use Eq, and Eq is a bit easier for newcomers in that the word itself resembles what the type class is all about (a function describing the meaning of equals/equality).

I just ran into some initial push-back about the library from team members because of naming like that. But, again, they eventually got over it, and, after that, the name no longer matters. I imagine other teams have had a similar experience.

It's just a matter of taste though, really. Honestly, I'd prefer sticking to terminology like bind over chain also, just to be consistent with Haskell/PureScript. I find it difficult when teaching people, because the names for these concepts are often different across libraries/languages... another ex: pure/point/of... but I know FantasyLand/Static Land have already bought into a specific naming convention.

joshburgess commented 5 years ago

On the fluent API subject: Is this something commonly found in Scala code? I'm assuming we have a number of Scala users weighing in here?

Personally, I'm not a fan of the fluent API. I can deal with it, but it's definitely not my preference. It has a much more OOP-ish feel to it, it bloats the shipped code, it complicates the internal implementations by introducing state in the this keyword (this really hurts the readability of the implementations, IMO), etc.

I much prefer just working with data & standalone functions. However, the limitations of TypeScript's inference algorithm when trying to work in a functional composition style (compose/pipe) do complicate things a bit here. I believe they've improved support for propagating generics recently though? But I'm assuming the right-to-left inference issues are still there...

joshburgess commented 5 years ago
However, as the Fluent interface usage adds some runtime overhead, people will choose between speed and ease of writing, code base styles will become heterogeneous and more verbose, adding to the noise.
I'm afraid the DX experience will just become bad.

I'm not sure about this. Inference issues aside, quite a lot of people who prefer a FP style were already using libraries like Ramda and lodash/fp which take a data-last & "functions only" (no fluent APIs) approach. Different than native JavaScript, yes, but similar to many of the languages that inspired the libraries.

Stream libraries like most and rxjs also migrated to more compositional style APIs.

mattiamanzati commented 5 years ago

I am also not sure if it actually introduces some overhead. Ideally the memory footprint should be lower, because of classes being less cheaper to instantiate. But tbh some tests needs to be done in this topic.

grossbart commented 5 years ago

Thanks for the examples, @gcanti, the fluent() one doesn't look as bad as I thought … Still drawn to @sledorze's arguments, a bit πŸ˜„

I tried to research the pipe/compose inference in TypeScript a bit and haven't found it to be resolved, unfortunately. This solves part of the problem, but not all (cf. this list of issues, haven't read them all).

For the sake of seeing a variety of approaches: this would be a version of pipe that can infer generic arguments properly. Very inflexible, heretic even, but a middle ground that TypeScript can type just so we can compare it to the other solutions.

// Not a good example because the pipe depends on the data
const inflexiblePipe = <A, B, C>(x: A, f: (x: A) => B, g: (x: B) => C): C => {
  return g(f(x));
}

const foo = inflexiblePipe(
  fromNullable(1),
  // But types can be inferred
  flatMap(n => (n === 0 ? none : some(1 / n))),
  lift(n => n * 2),
)
sledorze commented 5 years ago

@mattiamanzati actually, Fluent is a class.

mattiamanzati commented 5 years ago

@sledorze Oh, yeah. I missed that in the calculation. Nevermind, my guess was wrong! xD

mattiamanzati commented 5 years ago

But, tbh, if we were looking for performance, tbh the Fluent class can break the immutability rule and just mutate its internal value. It's only a builder afterall, so mutability on Fluent should'nt break anything

sledorze commented 5 years ago

@mattiamanzati nothing prevents one to reuse a Fluent value AFAIK, defeating that option for optimisation.

Going back to the proposed new idiomatic fp-ts way of using combinators; what do we propose for combinators with colliding names. What's the best DX for that?

One comment per proposal

sledorze commented 5 years ago

1) On first usage, lets say of map, the IDE proposes to import several possibilities. one choose the option one.

import { map } from 'fp-ts/lib/Option'

so now he wants to use map for array. The IDE does not propose anything as there's already a map in scope.

sledorze commented 5 years ago

2) The developper explicitly import the option module

import * as o from 'fp-ts/lib/Option'

And then use it like so o.map

When he wants to use Array's map, he adds

import * as a from 'fp-ts/lib/Array'

etc..

sledorze commented 5 years ago

3) The developper explicitly alias the imported map from Option

import { map as mapOption } from 'fp-ts/lib/Option'

then when he wants the array map, it does

import { map as mapArray } from 'fp-ts/lib/Array'
sledorze commented 5 years ago

4) ( A variation of 3) ) Somewhere the developper has prepared a reusable version of reexported prefixed combinators and can directly invoque the specialised version in its code.

import { mapArray } from 'whatever/Array'

or, if included in fp-ts:

import { mapArray } from 'fp-ts/lib/Array'

the great part is easy discovery and non conflicting imports

sledorze commented 5 years ago

Which one would you fp-ts to adopt / people would use / your code base will ends with?

Do you see some other ways?

(please πŸ‘ or πŸ‘Ž)

Wenqer commented 5 years ago

@sledorze do we have an option to introduce namespaces for modules? For example Option module:

export type Option<A> = Some<A> | None
export namespace Option {
  export const some = <A>(a: A): Option<A> => {
    return { _tag: 'Some', value: a }
  }

  export const map = <A, B>(ma: Option<A>, f: (a: A) => B): Option<B> => {
    return isNone(ma) ? ma : some(f(ma.value))
  }
}

or Array module:

export namespace Array {
  export const map = <A, B>(fa: Array<A>, f: (a: A) => B): Array<B> => {
    return fa.map(a => f(a))
  }
}

So usage of it would be just:

import { Option } from 'fp-ts/lib/Option'
import { Array } from 'fp-ts/lib/Array'

Option.map(Option.some('foo'), a => a + 'bar') // Some('foobar')

Array.map([1,2,3], x => x * 2) // [2, 4, 6]
sledorze commented 5 years ago

@Wenqer we're close to 2. + we gain stable names however I'm not sure about the tradeoffs (I never use namespaces myself).

gcanti commented 5 years ago

@mattiamanzati Re: benchmark. FWIW running the following computation

fluent(some(1))
  .map(n => n * 2)
  .chain(n => (n > 2 ? some(n) : none))
  .map(n => n + 1).value

with fp-ts@latest and the fp-ts#v2 branch I get the following results:

(*)

option.map(option.chain(option.map(some(1), n => n * 2), n => (n > 2 ? some(n) : none)), n => n + 1)