Fp-ts: ado

Created on 17 Jan 2019  路  26Comments  路  Source: gcanti/fp-ts

This is a feature proposal.

I'm not sure if that repo would collect control flow helpers, but seeing array comprehensions, it may actually be the case.

Motivation:

Combining several applicatives can be very verbose.
I've recently tried to use the solution below to ease imitating an applicative do (just some structural typings on top of Record sequence).

Usage

  const ado = sequenceRecord(either)
  ado({
    availability: parseAvailability(ticket), // Either monad base result
    rooms: parseRooms(ticket.rooming) // Either monad base result
  }).map(({ availability, rooms }) => myResult(
    availability,
    rooms
  ))

Note that one may take a better advantage of Validation from which you can enforce the error type via an Applicative2C

Code

import {
  Applicative,
  Applicative1,
  Applicative2,
  Applicative2C,
  Applicative3,
  Applicative3C
  } from 'fp-ts/lib/Applicative'
import {
  HKT,
  Type,
  Type2,
  Type3,
  URIS,
  URIS2,
  URIS3
  } from 'fp-ts/lib/HKT'
import { sequence } from 'fp-ts/lib/Record'

// tslint:disable: only-arrow-functions
export function sequenceRecord<F extends URIS3>(
  F: Applicative3<F>
): (<U, L, A extends Record<string, Type3<F, U, L, any>>>(
  a: A
) => Type3<
  F,
  { [k in keyof typeof a]: typeof a[k]['_U'] }[keyof typeof a],
  { [k in keyof typeof a]: typeof a[k]['_L'] }[keyof typeof a],
  { [k in keyof typeof a]: typeof a[k]['_A'] }
>)
export function sequenceRecord<F extends URIS3, U, L>(
  F: Applicative3C<F, U, L>
): (<A extends Record<string, Type3<F, U, L, any>>>(
  a: A
) => Type3<
  F,
  { [k in keyof typeof a]: typeof a[k]['_U'] }[keyof typeof a],
  { [k in keyof typeof a]: typeof a[k]['_L'] }[keyof typeof a],
  { [k in keyof typeof a]: typeof a[k]['_A'] }
>)
export function sequenceRecord<F extends URIS2>(
  F: Applicative2<F>
): (<L, A extends Record<string, Type2<F, L, any>>>(
  a: A
) => Type2<
  F,
  { [k in keyof typeof a]: typeof a[k]['_L'] }[keyof typeof a],
  { [k in keyof typeof a]: typeof a[k]['_A'] }
>)
export function sequenceRecord<F extends URIS2, L>(
  F: Applicative2C<F, L>
): (<A extends Record<string, Type2<F, L, any>>>(
  a: A
) => Type2<
  F,
  { [k in keyof typeof a]: typeof a[k]['_L'] }[keyof typeof a],
  { [k in keyof typeof a]: typeof a[k]['_A'] }
>)
export function sequenceRecord<F extends URIS>(
  F: Applicative1<F>
): (<A extends Record<string, Type<F, A>>>(
  a: A
) => Type<F, { [k in keyof typeof a]: typeof a[k]['_A'] }>)
export function sequenceRecord<F extends URIS>(
  F: Applicative<F>
): (<A extends Record<string, HKT<F, A>>>(
  a: A
) => Type<F, { [k in keyof typeof a]: typeof a[k]['_A'] }>)
export function sequenceRecord<F extends URIS>(
  F: Applicative<F>
): (<A extends Record<string, HKT<F, any>>>(
  a: A
) => Type<F, { [k in keyof typeof a]: typeof a[k]['_A'] }>) {
  return sequence(F) as any
}

discussion new feature

Most helpful comment

@mattiamanzati nice! thank you

@sledorze getting a union was the last bit I didn't like, if this works as expected we can move sequenceS back (from fp-ts-config)

All 26 comments

If I got this right, to obtain similar results I usually do:

import { sequenceT } from 'fp-ts/lib/Apply';

sequenceT(either)(
  parseAvailability(ticket),
  parseRooms(ticket.rooming)
).map(([availability, rooms]) => myResult(
  availability,
  rooms
))

Regarding the Record version, see also https://github.com/gcanti/fp-ts/pull/686#discussion_r245316168

@giogonzo thanks, that's very good to know!

Would be nice if / when we can leverage mapped tuples so that there's no restriction on the number of args.
Also regarding Records, the feature is only useful if we also map the codomain.

Also regarding Records, the feature is only useful if we also map the codomain.

Right, completely missed this. It wouldn't even be a Record in TS lingo.

To be honest then, I only see this proposal as an alternative syntax for the solution with sequenceT

@giogonzo we now use that pattern a lot, it removes (so) much boilerplate and is safer than sequenceT (not mismatch in the order of args when types are the same).

not mismatch in the order of args when types are the same

good point.

Rethinking about this, it would be nice to have it. The only problem I see is the naming then (Record has a fixed codomain in TS and fp-ts), and ado seems just too much (this helper does less things)

I'm totally open for a naming suggestion and agree on both args.

@sledorze { [k in keyof typeof a]: typeof a[k]['_L'] }[keyof typeof a] in

export function sequenceRecord<F extends URIS2>(
  F: Applicative2<F>
): (<L, A extends Record<string, Type2<F, L, any>>>(
  a: A
) => Type2<
  F,
  { [k in keyof typeof a]: typeof a[k]['_L'] }[keyof typeof a], // <= union of errors
  { [k in keyof typeof a]: typeof a[k]['_A'] }
>)

outputs a union (of errors), is it intended?

Yes as the L is not fixed.

A solution would be to allow only curried types APPLICATIVE2C for instance

Note that the sig may be simplified for those cases actually..

Yeah, we should try to come up with a good alternative of Record. I need an alternative name also to rename Semigroup.getRecordSemigroup and Monoid.getRecordMonoid.

Struct?

I particularly liked your proposal to use LabeledProduct and IndexedProduct (for tuples), possibly shortened when convenient (e.g. sequenceLP, sequenceIP): leaves no room for interpretation. But this is a huge breaking (or a lot of burden to maintain multiple renames).

I like Struct

We could use Tuple for tuples and Struct for records

  • Semigroup.getRecordSemigroup -> Semigroup.getStructSemigroup
  • Semigroup.getProductSemigroup -> Semigroup.getTupleSemigroup
  • Monoid.getRecordMonoid -> Monoid.getStructMonoid
  • Monoid.getProductMonoid -> Monoid.getTupleMonoid

sequenceT is ok (T means Tuple)

sequenceRecord (this issue) -> sequenceS (S means Struct)

Like it too

@sledorze I see two problems with this combinator

1) (minor) I'm not a fan of that union of errors (and restricting the usage to curried type classes would make the combinator much less useful)
2) (major) is leaking _A, _L, etc... (which btw I plan to remove in next releases if possible)

The second problem looks fixable using Type* instead of HKT* + conditional types..

export function sequenceRecord<F extends URIS2>(
  F: Applicative2<F>
): (<R extends Record<string, Type2<F, any, any>>>(
  a: R
) => Type2<
  F,
  { [K in keyof R]: R[K] extends Type2<F, infer L, any> ? L : never }[keyof R],
  { [K in keyof R]: R[K] extends Type2<F, any, infer A> ? A : never }
>)
export function sequenceRecord<F extends URIS>(F: Applicative<F>): any {
  return sequence(F)
}

..although I'm not a fan of conditional types either.

About 1;
I understand were you're coming from; closed unions and nominal types have nice properties and predicates are non ambiguous.
The usage here (control flow) requires precision on the errors, leaving the end user to deal with it (not like some Scala stories where some errors types are lost..).
Like you said, curried type classes are very restrictives.. I think it's a tradeoff question.
I would just state that the pattern is very important for the Developper Experience, I would love to have it baked into fp-ts.

About 2;
Yes we can migrate.

3) On conditional types; I'm not very confidant (as with all type level solutions) in typescript.
But, with a good automatic type level test, we can track regressions.
Also maybe we can think of another encoding..

I think it's a tradeoff question

I agree, that's why I tagged 1) with "minor", while I'm not a fan of a combinator which fuses the error side automatically, IMO is not such a big problem to reject a useful combinator.

Also it worths noting that this is a _technical_ problem, I just can't find a way to enforce the left side, but this doesn't mean we can't come up with a solution in the future, either with a different encoding or with a different TypeScript version with better inference. So we could clearly state in the documentation that mixing different types on the left side is not warranted and could change in next releases.

p.s.
On a side note, maybe we should create a fp-ts-contrib (much like io-ts-types) where we can put experimental features / useful combinators that we don't want to add to the core

p.s.
On a side note, maybe we should create a fp-ts-contrib (much like io-ts-types) where we can put experimental features / useful combinators that we don't want to add to the core

I've thought about this before. I was considering fp-ts-ext (ext for "extended")... but fp-ts-contrib works too.

I think it would be cool to see an fp-ts organization account that all of the fp-ts ecosystem libraries lived under.

@joshburgess discussed here:
https://github.com/gcanti/fp-ts/issues/414

@gcanti I think we're waiting for a new repo :)

@gcanti
And now I'm hesitating to wait for the phantom drop to happen.. :)
Should I?

@sledorze It would be nice to require an instance of Apply instead of Applicative, which means to enforce a non empty record

import { Apply2, Apply } from '../src/Apply'
import { URIS2, Type2, URIS, HKT } from '../src/HKT'

type EnforceNonEmptyRecord<R> = keyof R extends never ? never : R

export function sequenceS<F extends URIS2>(
  F: Apply2<F>
): (<R extends Record<string, Type2<F, any, any>>>(
  r: EnforceNonEmptyRecord<R>
) => Type2<
  F,
  { [K in keyof R]: R[K] extends Type2<F, infer L, any> ? L : never }[keyof R],
  { [K in keyof R]: R[K] extends Type2<F, any, infer A> ? A : never }
>)
export function sequenceS<F extends URIS>(
  F: Apply<F>
): (r: Record<string, HKT<F, any>>) => HKT<F, Record<string, any>> {
  return r => {
    const keys = Object.keys(r)
    const fst = keys[0]
    const others = keys.slice(1)
    let fr: HKT<F, Record<string, any>> = F.map(r[fst], a => ({ [fst]: a }))
    for (const key of others) {
      fr = F.ap(
        F.map(fr, r => (a: any) => {
          r[key] = a
          return r
        }),
        r[key]
      )
    }
    return fr
  }
}

import { either, right, left } from '../src/Either'

const ado = sequenceS(either)

console.log(ado({})) // static error
console.log(ado({ a: right(1) })) // right(1)
console.log(ado({ a: right(1), b: right(2) })) // right({ a: 1, b: 2 })
console.log(ado({ a: right(1), b: left('error') })) // left('error')

Another solution would be to define sequenceS as this one, which basically forces TS to first infer L, then R is inferred accordingly. Seems to be working with TS 3.3.3.

import { URIS2, Type2 } from 'fp-ts/lib/HKT'
import { Apply2 } from 'fp-ts/lib/Apply'
import { Either, either } from 'fp-ts/lib/Either'

type EnforceNonEmptyRecord<R> = keyof R extends never ? never : R

export declare function sequenceS<F extends URIS2>(
  F: Apply2<F>
): <L, R extends Record<string, Type2<F, any, any>>>(
  r: EnforceNonEmptyRecord<R> & Record<string, Type2<F, L, any>>
) => Type2<F, L, { [K in keyof R]: R[K] extends Type2<F, any, infer A> ? A : never }>

declare const fa: Either<string, number>
declare const fb: Either<string, boolean>
declare const fc: Either<string, string>

const f = sequenceS(either)
const x = f({ fa, fb, fc }) // => Either<string, {  fa: number;  fb: boolean;  fc: string;}>

declare const fd: Either<boolean, string>
const y = f({fa, fb, fc, fd }) // => Compiler error, boolean is not assignable to string

declare const fe: Either<boolean | string, string>
const z = f({fa, fb, fc, fe }) // => Left is widened to most "accomodating" type, which is string | boolean. Either<string | boolean, {  fa: number;  fb: boolean;  fc: string;  fd: string;  fe: string;}>

declare const ff: Either<number | boolean, string>
const w = f({fa, fb, fc, ff }) // => Compiler error, number | boolean is not assignable to string

I don't know much about sequence, so I don't know if the behaviour is "acceptable".

image

@mattiamanzati nice! thank you

@sledorze getting a union was the last bit I didn't like, if this works as expected we can move sequenceS back (from fp-ts-config)

Thanks and sorry for the late response, overly busy..

Was this page helpful?
0 / 5 - 0 ratings

Related issues

denistakeda picture denistakeda  路  4Comments

miguelferraro picture miguelferraro  路  3Comments

josete89 picture josete89  路  3Comments

amaurymartiny picture amaurymartiny  路  4Comments

bobaaaaa picture bobaaaaa  路  4Comments