Repository: gcanti/fp-ts (ts)

Issue:
Tutorial: fp-ts-to-the-max-I.ts with Do-notation
## 📖 Documentation

Thank you for your contributions.

I suggest you put some syntactic sugar for monad chains in the tutorial examples. While you prepared the good tutorial example, it was painful for me to read and understand what it is doing. (I'm still a beginner) I managed to write a runnable code using the Do-notation, provided in `fp-ts-contrib`. I think it needs kind of for-comprehension in scala or do-notation in haskell for better readability and usability. For now, `Do` in `fp-ts-contrib` may be a good choice.

### tutorials/fp-ts-to-the-max-I.ts

```
import * as T from 'fp-ts/lib/Task'
import { Task } from 'fp-ts/lib/Task'
import * as C from 'fp-ts/lib/Console'
import { createInterface } from 'readline'
import * as O from 'fp-ts/lib/Option'
import { Option } from 'fp-ts/lib/Option'
import * as R from 'fp-ts/lib/Random'
import { Do } from 'fp-ts-contrib/lib/Do'

// Helpers

const getStrLn: Task<string> = () =>
  new Promise(resolve => {
    const rl = createInterface({
      input: process.stdin,
      output: process.stdout,
    })
    rl.question('> ', answer => {
      rl.close()
      resolve(answer)
    })
  })

const putStrLn = (message: string): Task<void> => T.fromIO(C.log(message))

const random = T.fromIO(R.randomInt(1, 5))

const parse = (s: string): Option<number> => {
  const i = +s
  return isNaN(i) || i % 1 !== 0 ? O.none : O.some(i)
}

// Game

const checkContinue = (name: string): Task<boolean> =>
  Do(T.task)
    .do(putStrLn(`Do you want to continue, ${name}?`))
    .bind('answer', getStrLn)
    .bindL('result', ({ answer }) => {
      switch (answer.toLowerCase()) {
        case 'y':
          return T.of(true)
        case 'n':
          return T.of(false)
        default:
          return checkContinue(name)
      }
    })
    .return(({ result }) => result)

const parseFailureMessage = putStrLn('You did not enter an integer!')

const gameLoop = (name: string): Task<void> =>
  Do(T.task)
    .bind('secret', random)
    .do(putStrLn(`Dear ${name}, please guess a number from 1 to 5`))
    .bind('guess', getStrLn)
    .doL(({ secret, guess }) =>
      O.fold(
        () => parseFailureMessage,
        (x: number) =>
          x === secret
            ? putStrLn(`You guessed right, ${name}!`)
            : putStrLn(`You guessed wrong, ${name}! The number was: ${secret}`),
      )(parse(guess)),
    )
    .bind('shouldContinue', checkContinue(name))
    .doL(({ shouldContinue }) =>
      shouldContinue ? gameLoop(name) : T.of(undefined),
    )
    .return(_ => undefined)

const nameMessage = putStrLn('What is your name?')

const askName = Do(T.task)
  .do(nameMessage)
  .bind('name', getStrLn)
  .return(({ name }) => name)

const main: Task<void> = Do(T.task)
  .bind('name', askName)
  .doL(({ name }) => putStrLn(`Hello, ${name} welcome to the game!`))
  .doL(({ name }) => gameLoop(name))
  .return(_ => undefined)

main()
```


Pull request:
rewrite fp-ts-to-the-max using pipe and latest APIs, closes #1065

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 26cf0caf..8d8ae630 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -37,6 +37,8 @@ high state of flux, you're at risk of it changing without notice.
     - add `chainEither`, `chainIOEither`, `chainTaskEither` (@gcanti)
   - `StateReaderTaskEither`
     - add `chainEither`, `chainIOEither`, `chainTaskEither`, `chainReaderTaskEither` (@gcanti)
+  - `function`
+    - add `tupled`, `untupled` functions, closes #1062 (@gcanti)
 - **Deprecation**
   - `Ord`
     - deprecate `getSemigroup` in favor of `getMonoid` (@gcanti)
diff --git a/docs/modules/function.ts.md b/docs/modules/function.ts.md
index 290c6d3b..fd953571 100644
--- a/docs/modules/function.ts.md
+++ b/docs/modules/function.ts.md
@@ -32,6 +32,8 @@ Added in v2.0.0
 - [increment (function)](#increment-function)
 - [not (function)](#not-function)
 - [tuple (function)](#tuple-function)
+- [tupled (function)](#tupled-function)
+- [untupled (function)](#untupled-function)
 
 ---
 
@@ -338,3 +340,37 @@ export function tuple<T extends Array<any>>(...t: T): T { ... }
 ```
 
 Added in v2.0.0
+
+# tupled (function)
+
+Creates a tupled version of this function: instead of `n` arguments, it accepts a single tuple argument.
+
+**Signature**
+
+```ts
+export function tupled<A extends Array<unknown>, B>(f: (...a: A) => B): (a: A) => B { ... }
+```
+
+**Example**
+
+```ts
+import { tupled } from 'fp-ts/lib/function'
+
+const add = tupled((x: number, y: number): number => x + y)
+
+assert.strictEqual(add([1, 2]), 3)
+```
+
+Added in v2.4.0
+
+# untupled (function)
+
+Inverse function of `tupled`
+
+**Signature**
+
+```ts
+export function untupled<A extends Array<unknown>, B>(f: (a: A) => B): (...a: A) => B { ... }
+```
+
+Added in v2.4.0
diff --git a/dtslint/ts3.5/index.ts b/dtslint/ts3.5/index.ts
index 2f15288d..b00a155e 100644
--- a/dtslint/ts3.5/index.ts
+++ b/dtslint/ts3.5/index.ts
@@ -177,20 +177,14 @@ interface fromNullableTest2 {
 }
 declare const fromNullableTest3: <Key extends keyof fromNullableTest2>(key: Key) => fromNullableTest2[Key]
 // $ExpectType Option<number>
-Fu.flow(
-  fromNullableTest3,
-  O.fromNullable
-)('foo')
+Fu.flow(fromNullableTest3, O.fromNullable)('foo')
 
 //
 // Either
 //
 
 // $ExpectType Either<string, number>
-Fu.flow(
-  fromNullableTest3,
-  E.fromNullable('error')
-)('foo')
+Fu.flow(fromNullableTest3, E.fromNullable('error'))('foo')
 
 //
 // HKT
@@ -375,10 +369,13 @@ Fu.tuple(1, 'a') // $ExpectType [number, string]
 Fu.tuple(1, 'a', true) // $ExpectType [number, string, boolean]
 
 // $ExpectType <A>(head: A, tail: A[]) => Option<A>
-Fu.flow(
-  A.cons,
-  A.head
-)
+Fu.flow(A.cons, A.head)
+
+// tupled
+Fu.tupled(A.insertAt) // $ExpectType <A>(a: [number, A]) => (as: A[]) => Option<A[]>
+
+// untupled
+Fu.untupled(Fu.tupled(A.insertAt)) // $ExpectType <A>(i: number, a: A) => (as: A[]) => Option<A[]>
 
 //
 // Filterable overloads
diff --git a/src/function.ts b/src/function.ts
index b4737351..3391c343 100644
--- a/src/function.ts
+++ b/src/function.ts
@@ -273,3 +273,28 @@ export function decrement(n: number): number {
 export function absurd<A>(_: never): A {
   throw new Error('Called `absurd` function which should be uncallable')
 }
+
+/**
+ * Creates a tupled version of this function: instead of `n` arguments, it accepts a single tuple argument.
+ *
+ * @example
+ * import { tupled } from 'fp-ts/lib/function'
+ *
+ * const add = tupled((x: number, y: number): number => x + y)
+ *
+ * assert.strictEqual(add([1, 2]), 3)
+ *
+ * @since 2.4.0
+ */
+export function tupled<A extends Array<unknown>, B>(f: (...a: A) => B): (a: A) => B {
+  return a => f(...a)
+}
+
+/**
+ * Inverse function of `tupled`
+ *
+ * @since 2.4.0
+ */
+export function untupled<A extends Array<unknown>, B>(f: (a: A) => B): (...a: A) => B {
+  return (...a) => f(a)
+}
diff --git a/tutorials/fp-ts-to-the-max-I.ts b/tutorials/fp-ts-to-the-max-I.ts
index 33d71bf8..ce1240f4 100644
--- a/tutorials/fp-ts-to-the-max-I.ts
+++ b/tutorials/fp-ts-to-the-max-I.ts
@@ -1,14 +1,18 @@
 import { createInterface } from 'readline'
+import { sequenceS } from '../src/Apply'
 import { log } from '../src/Console'
-import { fold, none, Option, some } from '../src/Option'
+import { flow } from '../src/function'
+import * as O from '../src/Option'
+import { pipe } from '../src/pipeable'
 import { randomInt } from '../src/Random'
-import { Task, task } from '../src/Task'
+import * as T from '../src/Task'
 
 //
 // helpers
 //
 
-const getStrLn: Task<string> = () =>
+// read from standard input
+const getStrLn: T.Task<string> = () =>
   new Promise(resolve => {
     const rl = createInterface({
       input: process.stdin,
@@ -20,60 +24,76 @@ const getStrLn: Task<string> = () =>
     })
   })
 
-const putStrLn = (message: string): Task<void> => task.fromIO(log(message))
+// write to standard output
+const putStrLn = flow(log, T.fromIO)
 
-const random = task.fromIO(randomInt(1, 5))
+// ask something and get the answer
+function ask(question: string): T.Task<string> {
+  return pipe(
+    putStrLn(question),
+    T.chain(() => getStrLn)
+  )
+}
+
+// get a random int between 1 and 5
+const random = T.fromIO(randomInt(1, 5))
 
-const parse = (s: string): Option<number> => {
+// parse a string to an integer
+function parse(s: string): O.Option<number> {
   const i = +s
-  return isNaN(i) || i % 1 !== 0 ? none : some(i)
+  return isNaN(i) || i % 1 !== 0 ? O.none : O.some(i)
 }
 
 //
 // game
 //
 
-const checkContinue = (name: string): Task<boolean> => {
-  const put = putStrLn(`Do you want to continue, ${name}?`)
-  const get = task.chain(put, () => getStrLn)
-  const answer = task.chain(get, answer => {
-    switch (answer.toLowerCase()) {
-      case 'y':
-        return task.of(true)
-      case 'n':
-        return task.of(false)
-      default:
-        return checkContinue(name)
-    }
-  })
-  return answer
+function shouldContinue(name: string): T.Task<boolean> {
+  return pipe(
+    ask(`Do you want to continue, ${name} (y/n)?`),
+    T.chain(answer => {
+      switch (answer.toLowerCase()) {
+        case 'y':
+          return T.of(true)
+        case 'n':
+          return T.of(false)
+        default:
+          return shouldContinue(name)
+      }
+    })
+  )
 }
 
-const parseFailureMessage = putStrLn('You did not enter an integer!')
+// run `n` tasks in parallel
+const ado = sequenceS(T.task)
 
-const gameLoop = (name: string): Task<void> =>
-  task.chain(random, secret => {
-    const game = task.chain(putStrLn(`Dear ${name}, please guess a number from 1 to 5`), () =>
-      task.chain(getStrLn, guess =>
-        fold(
-          () => parseFailureMessage,
+function gameLoop(name: string): T.Task<void> {
+  return pipe(
+    ado({
+      secret: random,
+      guess: ask(`Dear ${name}, please guess a number from 1 to 5`)
+    }),
+    T.chain(({ secret, guess }) =>
+      pipe(
+        parse(guess),
+        O.fold(
+          () => putStrLn('You did not enter an integer!'),
           x =>
             x === secret
               ? putStrLn(`You guessed right, ${name}!`)
               : putStrLn(`You guessed wrong, ${name}! The number was: ${secret}`)
-        )(parse(guess))
+        )
       )
-    )
-    const doContinue = task.chain(game, () => checkContinue(name))
-    return task.chain(doContinue, shouldContinue => (shouldContinue ? gameLoop(name) : task.of(undefined)))
-  })
-
-const nameMessage = putStrLn('What is your name?')
-
-const askName = task.chain(nameMessage, () => getStrLn)
+    ),
+    T.chain(() => shouldContinue(name)),
+    T.chain(b => (b ? gameLoop(name) : T.of(undefined)))
+  )
+}
 
-const main: Task<void> = task.chain(askName, name =>
-  task.chain(putStrLn(`Hello, ${name} welcome to the game!`), () => gameLoop(name))
+const main: T.Task<void> = pipe(
+  ask('What is your name?'),
+  T.chainFirst(name => putStrLn(`Hello, ${name} welcome to the game!`)),
+  T.chain(gameLoop)
 )
 
 // tslint:disable-next-line: no-floating-promises
diff --git a/tutorials/fp-ts-to-the-max-II.ts b/tutorials/fp-ts-to-the-max-II.ts
index b1d7ec5a..77b2f161 100644
--- a/tutorials/fp-ts-to-the-max-II.ts
+++ b/tutorials/fp-ts-to-the-max-II.ts
@@ -1,34 +1,14 @@
 import { log } from '../src/Console'
 import { Kind, URIS } from '../src/HKT'
-import { none, Option, some, fold } from '../src/Option'
+import * as O from '../src/Option'
 import { randomInt } from '../src/Random'
 import * as T from '../src/Task'
 import { createInterface } from 'readline'
 import { State, state } from '../src/State'
 import { Monad1 } from '../src/Monad'
-
-//
-// helpers
-//
-
-const getStrLn: T.Task<string> = () =>
-  new Promise(resolve => {
-    const rl = createInterface({
-      input: process.stdin,
-      output: process.stdout
-    })
-    rl.question('> ', answer => {
-      rl.close()
-      resolve(answer)
-    })
-  })
-
-const putStrLn = (message: string): T.Task<void> => T.task.fromIO(log(message))
-
-const parse = (s: string): Option<number> => {
-  const i = +s
-  return isNaN(i) || i % 1 !== 0 ? none : some(i)
-}
+import { flow } from '../src/function'
+import { pipe, pipeable } from '../src/pipeable'
+import { sequenceS } from '../src/Apply'
 
 //
 // type classes
@@ -55,65 +35,108 @@ interface Main<F extends URIS> extends Program<F>, Console<F>, Random<F> {}
 
 const programTask: Program<T.URI> = {
   ...T.task,
-  finish: T.task.of
+  finish: T.of
 }
 
+// read from standard input
+const getStrLn: T.Task<string> = () =>
+  new Promise(resolve => {
+    const rl = createInterface({
+      input: process.stdin,
+      output: process.stdout
+    })
+    rl.question('> ', answer => {
+      rl.close()
+      resolve(answer)
+    })
+  })
+
+// write to standard output
+const putStrLn = flow(log, T.fromIO)
+
 const consoleTask: Console<T.URI> = {
   putStrLn,
   getStrLn
 }
 
 const randomTask: Random<T.URI> = {
-  nextInt: upper => T.task.fromIO(randomInt(1, upper))
+  nextInt: upper => T.fromIO(randomInt(1, upper))
 }
 
 //
 // game
 //
 
-const checkContinue = <F extends URIS>(F: Program<F> & Console<F>) => (name: string): Kind<F, boolean> => {
-  const put = F.putStrLn(`Do you want to continue, ${name}?`)
-  const get = F.chain(put, () => F.getStrLn)
-  const answer = F.chain(get, answer => {
-    switch (answer.toLowerCase()) {
-      case 'y':
-        return F.of(true)
-      case 'n':
-        return F.of(false)
-      default:
-        return checkContinue(F)(name)
-    }
-  })
-  return answer
+// parse a string to an integer
+function parse(s: string): O.Option<number> {
+  const i = +s
+  return isNaN(i) || i % 1 !== 0 ? O.none : O.some(i)
 }
 
-const gameLoop = <F extends URIS>(F: Main<F>) => (name: string): Kind<F, void> => {
-  const parseFailureMessage = F.putStrLn('You did not enter an integer!')
-
-  return F.chain(F.nextInt(5), secret => {
-    const game = F.chain(F.putStrLn(`Dear ${name}, please guess a number from 1 to 5`), () =>
-      F.chain(F.getStrLn, guess =>
-        fold(
-          () => parseFailureMessage,
-          x =>
-            x === secret
-              ? F.putStrLn(`You guessed right, ${name}!`)
-              : F.putStrLn(`You guessed wrong, ${name}! The number was: ${secret}`)
-        )(parse(guess))
-      )
+function main<F extends URIS>(F: Main<F>): Kind<F, void> {
+  // run `n` tasks in parallel
+  const ado = sequenceS(F)
+
+  const { chain, chainFirst } = pipeable(F)
+
+  // ask something and get the answer
+  const ask = (question: string): Kind<F, string> =>
+    pipe(
+      F.putStrLn(question),
+      chain(() => F.getStrLn)
     )
-    const doContinue = F.chain(game, () => checkContinue(F)(name))
-    return F.chain(doContinue, shouldContinue => (shouldContinue ? gameLoop(F)(name) : F.of<void>(undefined)))
-  })
-}
 
-const main = <F extends URIS>(F: Main<F>): Kind<F, void> => {
-  const nameMessage = F.putStrLn('What is your name?')
-  const askName = F.chain(nameMessage, () => F.getStrLn)
-  return F.chain(askName, name => F.chain(F.putStrLn(`Hello, ${name} welcome to the game!`), () => gameLoop(F)(name)))
+  const shouldContinue = (name: string): Kind<F, boolean> => {
+    return pipe(
+      ask(`Do you want to continue, ${name} (y/n)?`),
+      chain(answer => {
+        switch (answer.toLowerCase()) {
+          case 'y':
+            return F.of<boolean>(true)
+          case 'n':
+            return F.of<boolean>(false)
+          default:
+            return shouldContinue(name)
+        }
+      })
+    )
+  }
+
+  const gameLoop = (name: string): Kind<F, void> => {
+    return pipe(
+      ado({
+        secret: F.nextInt(5),
+        guess: ask(`Dear ${name}, please guess a number from 1 to 5`)
+      }),
+      chain(({ secret, guess }) =>
+        pipe(
+          parse(guess),
+          O.fold(
+            () => F.putStrLn('You did not enter an integer!'),
+            x =>
+              x === secret
+                ? F.putStrLn(`You guessed right, ${name}!`)
+                : F.putStrLn(`You guessed wrong, ${name}! The number was: ${secret}`)
+          )
+        )
+      ),
+      chain(() => shouldContinue(name)),
+      chain(b => (b ? gameLoop(name) : F.of<void>(undefined)))
+    )
+  }
+
+  return pipe(
+    ask('What is your name?'),
+    chainFirst(name => F.putStrLn(`Hello, ${name} welcome to the game!`)),
+    chain(gameLoop)
+  )
 }
 
-export const mainTask = main({ ...programTask, ...consoleTask, ...randomTask })
+export const mainTask = main({
+  ...programTask,
+  ...consoleTask,
+  ...randomTask
+})
 
 // tslint:disable-next-line: no-floating-promises
 // mainTask.run()
@@ -171,7 +194,12 @@ const randomTest: Random<URI> = {
   }
 }
 
-const mainTestTask = main({ ...programTest, ...consoleTest, ...randomTest })
+const mainTestTask = main({
+  ...programTest,
+  ...consoleTest,
+  ...randomTest
+})
+
 const testExample = new TestData(['Giulio', '1', 'n'], [], [1])
 
 import * as assert from 'assert'
@@ -185,7 +213,7 @@ assert.deepStrictEqual(mainTestTask(testExample), [
       'Hello, Giulio welcome to the game!',
       'Dear Giulio, please guess a number from 1 to 5',
       'You guessed right, Giulio!',
-      'Do you want to continue, Giulio?'
+      'Do you want to continue, Giulio (y/n)?'
     ],
     []
   )


Test patch:
diff --git a/test/function.ts b/test/function.ts
index 45c24e79..5d5263df 100644
--- a/test/function.ts
+++ b/test/function.ts
@@ -12,7 +12,9 @@ import {
   not,
   unsafeCoerce,
   absurd,
-  flow
+  flow,
+  tupled,
+  untupled
 } from '../src/function'
 
 const f = (n: number) => n + 1
@@ -78,4 +80,22 @@ describe('function', () => {
     assert.deepStrictEqual(flow(f, g, f, g, f, g, f, g)(2), 62)
     assert.deepStrictEqual(flow(f, g, f, g, f, g, f, g, f)(2), 63)
   })
+
+  it('tupled', () => {
+    const f1 = (a: number): number => a * 2
+    const f2 = (a: number, b: number): number => a + b
+    const u1 = tupled(f1)
+    const u2 = tupled(f2)
+    assert.deepStrictEqual(u1([1]), 2)
+    assert.deepStrictEqual(u2([1, 2]), 3)
+  })
+
+  it('untupled', () => {
+    const f1 = (a: [number]): number => a[0] * 2
+    const f2 = (a: [number, number]): number => a[0] + a[1]
+    const u1 = untupled(f1)
+    const u2 = untupled(f2)
+    assert.deepStrictEqual(u1(1), 2)
+    assert.deepStrictEqual(u2(1, 2), 3)
+  })
 })