LoginSignup
0
0

fp-tsの動画の内容の文字化 - 2

Last updated at Posted at 2024-04-22

fp-tsのtutorial動画があるが
中身を文字で見たかったが それを公開しているところがなかったので書いてみようと思った
※あまりにも長くなったので分割

動画

前回

Chapter 3: Either

OptionとEitherの比較



Eitherを利用するやり方



import { describe, it } from 'vitest'
import * as E from 'fp-ts/Either'
import { pipe } from 'fp-ts/lib/function'

describe('fp-ts Tutorial', () => {
  it('Either - Eitherを利用するやり方', () => {
    type Account = Readonly<{
      balance: number
      frozen: boolean
    }>
    type AccountFrozen = Readonly<{
      type: 'AccountFrozen'
      message: string
    }>
    type NotEnoughBalance = Readonly<{
      type: 'NotEnoughBalance'
      message: string
    }>

    const pay =
      (amount: number) =>
      (account: Account): E.Either<NotEnoughBalance | AccountFrozen, Account> =>
        account.frozen
          ? E.left({
              type: 'AccountFrozen',
              message: 'Cannot pay with a frozen account!'
            })
          : account.balance < amount
            ? E.left({
                type: 'NotEnoughBalance',
                message: `Cannot pay ${amount} with a balance of ${account.balance}!`
              })
            : E.right({
                ...account,
                balance: account.balance - amount
              })

    const account1: Account = {
      balance: 70,
      frozen: false
    }
    const account2: Account = {
      balance: 30,
      frozen: false
    }
    const account3: Account = {
      balance: 100,
      frozen: true
    }

    console.log(pipe(account1, pay(50)))
    console.log(E.right({ balance: 20, frozen: false }))
    console.log('-------------')

    console.log(pipe(account2, pay(50)))
    console.log(
      E.left({
        type: 'NotEnoughBalance',
        message: 'Cannot pay 50 with a balance of 30!'
      })
    )
    console.log('-------------')

    console.log(pipe(account3, pay(50)))
    console.log(
      E.left({
        type: 'AccountFrozen',
        message: 'Cannot pay with a frozen account!'
      })
    )
    console.log('-------------')
  })
})

実行結果

{ _tag: 'Right', right: { balance: 20, frozen: false } }
{ _tag: 'Right', right: { balance: 20, frozen: false } }
-------------
{
  _tag: 'Left',
  left: {
    type: 'NotEnoughBalance',
    message: 'Cannot pay 50 with a balance of 30!'
  }
}
{
  _tag: 'Left',
  left: {
    type: 'NotEnoughBalance',
    message: 'Cannot pay 50 with a balance of 30!'
  }
}
-------------
{
  _tag: 'Left',
  left: {
    type: 'AccountFrozen',
    message: 'Cannot pay with a frozen account!'
  }
}
{
  _tag: 'Left',
  left: {
    type: 'AccountFrozen',
    message: 'Cannot pay with a frozen account!'
  }
}
-------------

エラーをthrowするやり方



import { describe, it } from 'vitest'
import { pipe } from 'fp-ts/lib/function'

describe('fp-ts Tutorial', () => {
  it('Either - エラーをthrowするやり方', () => {
    type Account = Readonly<{
      balance: number
      frozen: boolean
    }>

    class AccountFrozenError extends Error {
      type = 'AccountFrozen'
      public constructor(message?: string) {
        super(message)
      }
    }
    class NotEnoughBalanceError extends Error {
      type = 'NotEnoughBalance'
      public constructor(message?: string) {
        super(message)
      }
    }

    const pay = (amount: number) => (account: Account) => {
      if (account.frozen) {
        throw new AccountFrozenError('Cannot pay with a frozen account!')
      } else if (account.balance < amount) {
        throw new NotEnoughBalanceError(
          `Cannot pay ${amount} with a balance of ${account.balance}!`
        )
      } else {
        return {
          ...account,
          balance: account.balance - amount
        }
      }
    }

    const account1: Account = {
      balance: 70,
      frozen: false
    }
    const account2: Account = {
      balance: 30,
      frozen: false
    }
    const account3: Account = {
      balance: 100,
      frozen: true
    }

    console.log(pipe(account1, pay(50)))
    try {
      console.log(pipe(account2, pay(50)))
    } catch (e) {
      console.log(e.message)
    }
    try {
      console.log(pipe(account3, pay(50)))
    } catch (e) {
      console.log(e.message)
    }
  })
})

実行結果

{ balance: 20, frozen: false }
Cannot pay 50 with a balance of 30!
Cannot pay with a frozen account!

E.fold(onLeft, onRight)






import { describe, it } from 'vitest'
import * as E from 'fp-ts/Either'
import { pipe } from 'fp-ts/lib/function'

describe('fp-ts Tutorial', () => {
  it('Either - E.fold(onLeft, onRight)', () => {
    type Cart = Readonly<{
      items: string[]
      total: number
    }>
    type Account = Readonly<{
      balance: number
      frozen: boolean
    }>
    type AccountFrozen = Readonly<{
      type: 'AccountFrozen'
      message: string
    }>
    type NotEnoughBalance = Readonly<{
      type: 'NotEnoughBalance'
      message: string
    }>

    const pay =
      (amount: number) =>
      (account: Account): E.Either<NotEnoughBalance | AccountFrozen, Account> =>
        account.frozen
          ? E.left({
              type: 'AccountFrozen',
              message: 'Cannot pay with a frozen account!'
            })
          : account.balance < amount
            ? E.left({
                type: 'NotEnoughBalance',
                message: `Cannot pay ${amount} with a balance of ${account.balance}!`
              })
            : E.right({
                ...account,
                balance: account.balance - amount
              })

    const checkout_sample = (cart: Cart) => (account: Account) =>
      pipe(
        account,
        pay(cart.total),
        E.fold(
          (e) => 'Handle error...',
          (a) => 'handle success...'
        )
      )
    const checkout = (cart: Cart) => (account: Account) =>
      pipe(
        account,
        pay(cart.total),
        E.foldW(
          (e) => e.message,
          (a) => a
        )
      )

    const account1: Account = {
      balance: 70,
      frozen: false
    }
    const account2: Account = {
      balance: 30,
      frozen: false
    }
    const account3: Account = {
      balance: 100,
      frozen: true
    }

    const cart1: Cart = {
      items: ['ice cream', 'waffle', 'candy'],
      total: 50
    }

    console.log(checkout_sample(cart1)(account1))
    console.log(checkout(cart1)(account1))
    console.log('-----------')
    console.log(checkout_sample(cart1)(account2))
    console.log(checkout(cart1)(account2))
    console.log('-----------')
    console.log(checkout_sample(cart1)(account3))
    console.log(checkout(cart1)(account3))
    console.log('-----------')
  })
})

実行結果

handle success...
{ balance: 20, frozen: false }
-----------
Handle error...
Cannot pay 50 with a balance of 30!
-----------
Handle error...
Cannot pay with a frozen account!
-----------

ts-adt - makeMatch

ts-adt の利用例

import { describe, it } from 'vitest'
import * as E from 'fp-ts/Either'
import { pipe } from 'fp-ts/lib/function'
import { makeMatch } from 'ts-adt/MakeADT'

describe('fp-ts Tutorial', () => {
  it('Either - ts-adt - makeMatch', () => {
    type Cart = Readonly<{
      items: string[]
      total: number
    }>
    type Account = Readonly<{
      balance: number
      frozen: boolean
    }>
    type AccountFrozen = Readonly<{
      type: 'AccountFrozen'
      message: string
    }>
    type NotEnoughBalance = Readonly<{
      type: 'NotEnoughBalance'
      message: string
    }>

    const matchError = makeMatch('type')

    const pay =
      (amount: number) =>
      (account: Account): E.Either<NotEnoughBalance | AccountFrozen, Account> =>
        account.frozen
          ? E.left({
              type: 'AccountFrozen',
              message: 'Cannot pay with a frozen account!'
            })
          : account.balance < amount
            ? E.left({
                type: 'NotEnoughBalance',
                message: `Cannot pay ${amount} with a balance of ${account.balance}!`
              })
            : E.right({
                ...account,
                balance: account.balance - amount
              })

    const checkout_sample = (cart: Cart) => (account: Account) =>
      pipe(
        account,
        pay(cart.total),
        E.fold(
          (e) => 'Handle error...',
          (a) => 'handle success...'
        )
      )
    const checkout = (cart: Cart) => (account: Account) =>
      pipe(
        account,
        pay(cart.total),
        E.foldW(
          matchError({
            AccountFrozen: (e) => e.message,
            NotEnoughBalance: (e) => e.message
          }),
          (a) => a
        )
      )

    const account1: Account = {
      balance: 70,
      frozen: false
    }
    const account2: Account = {
      balance: 30,
      frozen: false
    }
    const account3: Account = {
      balance: 100,
      frozen: true
    }

    const cart1: Cart = {
      items: ['ice cream', 'waffle', 'candy'],
      total: 50
    }

    console.log(checkout_sample(cart1)(account1))
    console.log(checkout(cart1)(account1))
    console.log('-----------')
    console.log(checkout_sample(cart1)(account2))
    console.log(checkout(cart1)(account2))
    console.log('-----------')
    console.log(checkout_sample(cart1)(account3))
    console.log(checkout(cart1)(account3))
    console.log('-----------')
  })
})

実行結果

handle success...
{ balance: 20, frozen: false }
-----------
Handle error...
Cannot pay 50 with a balance of 30!
-----------
Handle error...
Cannot pay with a frozen account!
-----------

Chapter 3.1: Either tryCatch

E.tryCatch


import { describe, it } from 'vitest'
import * as E from 'fp-ts/Either'

describe('fp-ts Tutorial', () => {
  it('Either tryCatch - E.tryCatch', () => {
    const jsonParse1 = (jsonText: string): E.Either<Error, unknown> => {
      try {
        const result = JSON.parse(jsonText)
        return E.right(result)
      } catch (e) {
        const error = e instanceof Error ? e : new Error(String(e))
        return E.left(error)
      }
    }
    const jsonParse2 = (jsonText: string): E.Either<Error, unknown> =>
      E.tryCatch(
        () => JSON.parse(jsonText),
        (e) => (e instanceof Error ? e : new Error(String(e)))
      )
    const jsonParse3 = (jsonText: string): E.Either<Error, unknown> =>
      E.tryCatch(() => JSON.parse(jsonText), E.toError)

    const jsonData1 = JSON.stringify({
      test_string: 'test value',
      test_number: 123
    })

    const jsonData2 = 'test'

    console.log(jsonData1)
    console.log(jsonData2)
    console.log('----------------')
    console.log(jsonParse1(jsonData1))
    console.log(jsonParse2(jsonData1))
    console.log(jsonParse3(jsonData1))
    console.log('----------------')
    console.log(jsonParse1(jsonData2).left.message)
    console.log(jsonParse2(jsonData2).left.message)
    console.log(jsonParse3(jsonData2).left.message)
    console.log('----------------')
  })
})

実行結果

{"test_string":"test value","test_number":123}
test
----------------
{
  _tag: 'Right',
  right: { test_string: 'test value', test_number: 123 }
}
{
  _tag: 'Right',
  right: { test_string: 'test value', test_number: 123 }
}
{
  _tag: 'Right',
  right: { test_string: 'test value', test_number: 123 }
}
----------------
Unexpected token e in JSON at position 1
Unexpected token e in JSON at position 1
Unexpected token e in JSON at position 1
----------------

E.tryCatchK - K means Kleisli


import { describe, it } from 'vitest'
import * as E from 'fp-ts/Either'

describe('fp-ts Tutorial', () => {
  it('Either tryCatch - E.tryCatchK', () => {
    const jsonParse3: (jsonText: string) => E.Either<Error, unknown> = (
      jsonText: string
    ): E.Either<Error, unknown> => E.tryCatch(() => JSON.parse(jsonText), E.toError)
    const jsonParseK4: (jsonText: string) => E.Either<Error, unknown> = E.tryCatchK(
      JSON.parse,
      E.toError
    )

    const jsonData1 = JSON.stringify({
      test_string: 'test value',
      test_number: 123
    })

    const jsonData2 = 'test'

    console.log(jsonData1)
    console.log(jsonData2)
    console.log('----------------')
    console.log(jsonParse3(jsonData1))
    console.log(jsonParseK4(jsonData1))
    console.log('----------------')
    console.log(jsonParse3(jsonData2).left.message)
    console.log(jsonParseK4(jsonData2).left.message)
    console.log('----------------')
  })
})

実行結果

{"test_string":"test value","test_number":123}
test
----------------
{
  _tag: 'Right',
  right: { test_string: 'test value', test_number: 123 }
}
{
  _tag: 'Right',
  right: { test_string: 'test value', test_number: 123 }
}
----------------
Unexpected token e in JSON at position 1
Unexpected token e in JSON at position 1
----------------

E.tryCatchK - 2


実行はできるが赤い背景のところでエラーが出る

import { describe, it } from 'vitest'
import * as E from 'fp-ts/Either'

describe('fp-ts Tutorial', () => {
  it('Either tryCatch - 2', () => {
    type JsonParseError = Readonly<{
      type: 'JsonParseError'
      error: Error
    }>

    const jsonParse5: (jsonText: string) => E.Either<JsonParseError, unknown> = (
      jsonText: string
    ): E.Either<Error, unknown> =>
-      E.tryCatch(
-        () => JSON.parse(jsonText),
-        (e) => ({
-          type: 'JsonParseError',
-          error: E.toError(e)
-        })
-      )
    const jsonParseK6: (jsonText: string) => E.Either<JsonParseError, unknown> = E.tryCatchK(
      JSON.parse,
      (e) => ({
        type: 'JsonParseError',
        error: E.toError(e)
      })
    )

    const jsonData1 = JSON.stringify({
      test_string: 'test value',
      test_number: 123
    })

    const jsonData2 = 'test'

    console.log(jsonData1)
    console.log(jsonData2)
    console.log('----------------')
    console.log(jsonParse5(jsonData1))
    console.log(jsonParseK6(jsonData1))
    console.log('----------------')
    console.log(jsonParse5(jsonData2).left.type)
    console.log(jsonParse5(jsonData2).left.error.message)
    console.log(jsonParseK6(jsonData2).left.type)
    console.log(jsonParseK6(jsonData2).left.error.message)
    console.log('----------------')
  })
})

実行結果

{"test_string":"test value","test_number":123}
test
----------------
{
  _tag: 'Right',
  right: { test_string: 'test value', test_number: 123 }
}
{
  _tag: 'Right',
  right: { test_string: 'test value', test_number: 123 }
}
----------------
JsonParseError
Unexpected token e in JSON at position 1
JsonParseError
Unexpected token e in JSON at position 1
----------------

※VSCodeなどでエラー表示されるが、実行は可能
※Type 'Either<{ type: string; error: Error; }, any>' is not assignable to type 'Either'.

Chapter 3.2: Either map, mapLeft, bimap

J.stringify

import { describe, it } from 'vitest'
import * as E from 'fp-ts/Either'
import * as J from 'fp-ts/Json'
import { pipe } from 'fp-ts/lib/function'

describe('fp-ts Tutorial', () => {
  it('Either map, mapLeft, bimap - J.stringify', () => {
    type JsonStringifyError = Readonly<{
      type: 'JsonStringifyError'
      error: Error
    }>
    type Response = Readonly<{
      body: string
      contentLength: number
    }>

    const createResponse1 = (payload: unknown): E.Either<JsonStringifyError, Response> =>
      pipe(payload, JSON.stringify)

    const createResponse2 = (payload: unknown): E.Either<JsonStringifyError, Response> =>
      pipe(payload, J.stringify)

    const jsonData = { balance: 100, success: true }

    console.log(createResponse1(jsonData))
    console.log(createResponse2(jsonData))
    console.log(E.right(jsonData))
  })
})

実行結果

{"balance":100,"success":true}
{ _tag: 'Right', right: '{"balance":100,"success":true}' }
{ _tag: 'Right', right: { balance: 100, success: true } }

E.map(f)

import { describe, it } from 'vitest'
import * as E from 'fp-ts/Either'
import * as J from 'fp-ts/Json'
import { pipe } from 'fp-ts/lib/function'

describe('fp-ts Tutorial', () => {
  it('Either map, mapLeft, bimap - E.map(f)', () => {
    type JsonStringifyError = Readonly<{
      type: 'JsonStringifyError'
      error: Error
    }>
    type Response = Readonly<{
      body: string
      contentLength: number
    }>

    const debug_log = <T>(x: T) => {
      console.log(x)
      return x
    }
    const createResponse = (payload: unknown): E.Either<JsonStringifyError, Response> =>
      pipe(
        payload,
        debug_log,
        J.stringify,
        debug_log,
        E.map((s) => ({
          body: s,
          contentLength: s.length
        }))
      )

    const jsonData = { balance: 100, success: true }

    console.log(createResponse(jsonData))
    console.log('--------------')
    console.log(
      E.right({
        body: JSON.stringify(jsonData),
        contentLength: JSON.stringify(jsonData).length
      })
    )
  })
})

実行結果

{ balance: 100, success: true }
{ _tag: 'Right', right: '{"balance":100,"success":true}' }
{
  _tag: 'Right',
  right: { body: '{"balance":100,"success":true}', contentLength: 30 }
}
--------------
{
  _tag: 'Right',
  right: { body: '{"balance":100,"success":true}', contentLength: 30 }
}

E.mapLeft(f)


import { describe, it } from 'vitest'
import * as E from 'fp-ts/Either'
import * as J from 'fp-ts/Json'
import { pipe } from 'fp-ts/lib/function'

describe('fp-ts Tutorial', () => {
  it('Either map, mapLeft, bimap - E.mapLeft(f)', () => {
    type JsonStringifyError = Readonly<{
      type: 'JsonStringifyError'
      error: Error
    }>
    type Response = Readonly<{
      body: string
      contentLength: number
    }>

    const debug_log = <T>(x: T) => {
      console.log(x)
      return x
    }
    const createResponse = (payload: unknown): E.Either<JsonStringifyError, Response> =>
      pipe(
        payload,
        debug_log,
        J.stringify,
        // debug_log,
        E.map((s) => ({
          body: s,
          contentLength: s.length
        })),
        E.mapLeft((e) => ({
          type: 'JsonStringifyError',
          error: E.toError(e)
        }))
      )

    const jsonDataOk = { balance: 100, success: true }
    const jsonDataNg = {}
    jsonDataNg.self = jsonDataNg

    console.log(createResponse(jsonDataOk))
    console.log('--------------')
    console.log(
      E.right({
        body: JSON.stringify(jsonDataOk),
        contentLength: JSON.stringify(jsonDataOk).length
      })
    )
    console.log('--------------')
    console.log(createResponse(jsonDataNg).left.error.message)
    console.log('--------------')
    console.log(
      E.left({
        type: 'JsonStringifyError',
        error: E.toError(
          TypeError(
            "Converting circular structure to JSON\n    --> starting at object with constructor 'Object'\n    --- property 'self' closes the circle"
          )
        )
      }).left.error.message
    )
  })
})

実行結果

{ balance: 100, success: true }
{
  _tag: 'Right',
  right: { body: '{"balance":100,"success":true}', contentLength: 30 }
}
--------------
{
  _tag: 'Right',
  right: { body: '{"balance":100,"success":true}', contentLength: 30 }
}
--------------
<ref *1> { self: [Circular *1] }
Converting circular structure to JSON
    --> starting at object with constructor 'Object'
    --- property 'self' closes the circle
--------------
Converting circular structure to JSON
    --> starting at object with constructor 'Object'
    --- property 'self' closes the circle

E.bimap(f)


import { describe, it } from 'vitest'
import * as E from 'fp-ts/Either'
import * as J from 'fp-ts/Json'
import { pipe } from 'fp-ts/lib/function'

describe('fp-ts Tutorial', () => {
  it('Either map, mapLeft, bimap - E.bimap(f)', () => {
    type JsonStringifyError = Readonly<{
      type: 'JsonStringifyError'
      error: Error
    }>
    type Response = Readonly<{
      body: string
      contentLength: number
    }>

    const debug_log = <T>(x: T) => {
      console.log(x)
      return x
    }
    const createResponse = (payload: unknown): E.Either<JsonStringifyError, Response> =>
      pipe(
        payload,
        debug_log,
        J.stringify,
        // debug_log,
        E.bimap(
          (e) => ({
            type: 'JsonStringifyError',
            error: E.toError(e)
          }),
          (s) => ({
            body: s,
            contentLength: s.length
          })
        )
      )

    const jsonDataOk = { balance: 100, success: true }
    const jsonDataNg = {}
    jsonDataNg.self = jsonDataNg

    console.log(createResponse(jsonDataOk))
    console.log('--------------')
    console.log(
      E.right({
        body: JSON.stringify(jsonDataOk),
        contentLength: JSON.stringify(jsonDataOk).length
      })
    )
    console.log('--------------')
    console.log(createResponse(jsonDataNg).left.error.message)
    console.log('--------------')
    console.log(
      E.left({
        type: 'JsonStringifyError',
        error: E.toError(
          TypeError(
            "Converting circular structure to JSON\n    --> starting at object with constructor 'Object'\n    --- property 'self' closes the circle"
          )
        )
      }).left.error.message
    )
  })
})

実行結果

{ balance: 100, success: true }
{
  _tag: 'Right',
  right: { body: '{"balance":100,"success":true}', contentLength: 30 }
}
--------------
{
  _tag: 'Right',
  right: { body: '{"balance":100,"success":true}', contentLength: 30 }
}
--------------
<ref *1> { self: [Circular *1] }
Converting circular structure to JSON
    --> starting at object with constructor 'Object'
    --- property 'self' closes the circle
--------------
Converting circular structure to JSON
    --> starting at object with constructor 'Object'
    --- property 'self' closes the circle

Chapter 3.3: Either flatMap (chain)

E.map(f) - E.flattenW





import { describe, it } from 'vitest'
import * as E from 'fp-ts/Either'
import { pipe } from 'fp-ts/lib/function'
import base64 from 'base-64'

describe('fp-ts Tutorial', () => {
  it('Either flatMap (chain) - E.map(f) - E.flattenW', () => {
    type User = Readonly<{
      id: number
      username: string
    }>
    type Base64DecodeError = Readonly<{
      type: 'Base64DecodeError'
      error: Error
    }>
    type JsonParseError = Readonly<{
      type: 'JsonParseError'
      error: Error
    }>

    const base64Decode = E.tryCatchK(
      base64.decode,
      (e): Base64DecodeError => ({
        type: 'Base64DecodeError',
        error: E.toError(e)
      })
    )

    const jsonParse: (jsonText: string) => E.Either<JsonParseError, unknown> = E.tryCatchK(
      JSON.parse,
      (e) => ({
        type: 'JsonParseError',
        error: E.toError(e)
      })
    )

    const debug_log = <T>(x: T) => {
      if (E.isLeft(x)) {
        if (E.isLeft(x.left)) {
          console.log(x.left.left.error.message)
        } else {
          console.log(x.left.error.message)
        }
      } else if (E.isRight(x)) {
        if (E.isLeft(x.right)) {
          console.log(x.right.left.error.message)
        } else {
          console.log(x)
        }
      } else {
        console.log(x)
      }
      return x
    }
    // const debug_log = <T>(x: T) => {
    //   console.log(x)
    //   return x
    // }

    const decodeUser = (encodedUser: string) =>
      pipe(encodedUser, debug_log, base64Decode, debug_log, E.map(jsonParse), debug_log, E.flattenW)

    const userOk: User = {
      id: 1,
      username: 'jimmy'
    }
    const encodedUserOk: string = base64.encode(JSON.stringify(userOk))

    const encodedUserNg1: string = ''
    const encodedUserNg2 = 1

    console.log(decodeUser(encodedUserOk))
    console.log('-----------------')
    console.log(decodeUser(encodedUserNg1).left.error.message)
    console.log('-----------------')
    console.log(decodeUser(encodedUserNg2).left.error.message)
  })
})

実行結果

eyJpZCI6MSwidXNlcm5hbWUiOiJqaW1teSJ9
{ _tag: 'Right', right: '{"id":1,"username":"jimmy"}' }
{
  _tag: 'Right',
  right: { _tag: 'Right', right: { id: 1, username: 'jimmy' } }
}
{ _tag: 'Right', right: { id: 1, username: 'jimmy' } }
-----------------

{ _tag: 'Right', right: '' }
Unexpected end of JSON input
Unexpected end of JSON input
-----------------
1
Invalid character: the string to be decoded is not correctly encoded.
Invalid character: the string to be decoded is not correctly encoded.
Invalid character: the string to be decoded is not correctly encoded.

E.flatMap(f)





import { describe, it } from 'vitest'
import * as E from 'fp-ts/Either'
import { pipe } from 'fp-ts/lib/function'
import base64 from 'base-64'

describe('fp-ts Tutorial', () => {
  it('Either flatMap (chain) - E.flatMap(f)', () => {
    type User = Readonly<{
      id: number
      username: string
    }>
    type Base64DecodeError = Readonly<{
      type: 'Base64DecodeError'
      error: Error
    }>
    type JsonParseError = Readonly<{
      type: 'JsonParseError'
      error: Error
    }>

    const base64Decode = E.tryCatchK(
      base64.decode,
      (e): Base64DecodeError => ({
        type: 'Base64DecodeError',
        error: E.toError(e)
      })
    )

    const jsonParse: (jsonText: string) => E.Either<JsonParseError, unknown> = E.tryCatchK(
      JSON.parse,
      (e) => ({
        type: 'JsonParseError',
        error: E.toError(e)
      })
    )

    const debug_log = <T>(x: T) => {
      if (E.isLeft(x)) {
        if (E.isLeft(x.left)) {
          console.log(x.left.left.error.message)
        } else {
          console.log(x.left.error.message)
        }
      } else if (E.isRight(x)) {
        if (E.isLeft(x.right)) {
          console.log(x.right.left.error.message)
        } else {
          console.log(x)
        }
      } else {
        console.log(x)
      }
      return x
    }
    // const debug_log = <T>(x: T) => {
    //   console.log(x)
    //   return x
    // }

    const decodeUser = (encodedUser: string) =>
      pipe(encodedUser, debug_log, base64Decode, debug_log, E.flatMap(jsonParse))

    const userOk: User = {
      id: 1,
      username: 'jimmy'
    }
    const encodedUserOk: string = base64.encode(JSON.stringify(userOk))

    const encodedUserNg1: string = ''
    const encodedUserNg2 = 1

    console.log(decodeUser(encodedUserOk))
    console.log('-----------------')
    console.log(decodeUser(encodedUserNg1).left.error.message)
    console.log('-----------------')
    console.log(decodeUser(encodedUserNg2).left.error.message)
  })
})

実行結果

eyJpZCI6MSwidXNlcm5hbWUiOiJqaW1teSJ9
{ _tag: 'Right', right: '{"id":1,"username":"jimmy"}' }
{ _tag: 'Right', right: { id: 1, username: 'jimmy' } }
-----------------

{ _tag: 'Right', right: '' }
Unexpected end of JSON input
-----------------
1
Invalid character: the string to be decoded is not correctly encoded.
Invalid character: the string to be decoded is not correctly encoded.

E.flatMap(f) - 2






import { describe, it } from 'vitest'
import * as E from 'fp-ts/Either'
import { pipe } from 'fp-ts/lib/function'
import base64 from 'base-64'

describe('fp-ts Tutorial', () => {
  it('Either flatMap (chain) - E.flatMap(f) - 2', () => {
    type User = Readonly<{
      id: number
      username: string
    }>
    type Base64DecodeError = Readonly<{
      type: 'Base64DecodeError'
      error: Error
    }>
    type JsonParseError = Readonly<{
      type: 'JsonParseError'
      error: Error
    }>
    type InvalidUserObject = Readonly<{
      type: 'InvalidUserObject'
      error: Error
    }>

    const base64Decode = E.tryCatchK(
      base64.decode,
      (e): Base64DecodeError => ({
        type: 'Base64DecodeError',
        error: E.toError(e)
      })
    )

    const jsonParse: (jsonText: string) => E.Either<JsonParseError, unknown> = E.tryCatchK(
      JSON.parse,
      (e) => ({
        type: 'JsonParseError',
        error: E.toError(e)
      })
    )

    const isUser = (u: unknown): u is User =>
      u &&
      typeof u === 'object' &&
      'id' in u &&
      typeof u.id === 'number' &&
      'username' in u &&
      typeof u.username === 'string'
        ? true
        : false

    const decodeUserObjectFromUnknown: (u: unknown) => E.Either<InvalidUserObject, User> =
      E.tryCatchK(
        (u) => {
          if (isUser(u)) {
            return u
          }

          throw Error(`${JSON.stringify(u)} is not User`)
        },
        (e) => ({
          type: 'InvalidUserObject',
          error: E.toError(e)
        })
      )

    const debug_log = <T>(x: T) => {
      if (E.isLeft(x)) {
        if (E.isLeft(x.left)) {
          console.log(x.left.left.error.message)
        } else {
          console.log(x.left.error.message)
        }
      } else if (E.isRight(x)) {
        if (E.isLeft(x.right)) {
          console.log(x.right.left.error.message)
        } else {
          console.log(x)
        }
      } else {
        console.log(x)
      }
      return x
    }
    // const debug_log = <T>(x: T) => {
    //   console.log(x)
    //   return x
    // }

    const decodeUser = (encodedUser: string) =>
      pipe(
        encodedUser,
        debug_log,
        base64Decode,
        debug_log,
        E.flatMap(jsonParse),
        debug_log,
        E.flatMap(decodeUserObjectFromUnknown)
      )

    const userOk: User = {
      id: 1,
      username: 'jimmy'
    }
    const encodedUserOk: string = base64.encode(JSON.stringify(userOk))

    const encodedUserNg1: string = ''
    const encodedUserNg2 = 1

    const userNg = {
      id: 1,
      name: 'jimmy'
    }
    const encodedUserNg3: string = base64.encode(JSON.stringify(userNg))

    console.log(decodeUser(encodedUserOk))
    console.log('-----------------')
    console.log(decodeUser(encodedUserNg1).left.error.message)
    console.log('-----------------')
    console.log(decodeUser(encodedUserNg2).left.error.message)
    console.log('-----------------')
    console.log(decodeUser(encodedUserNg3).left.error.message)
  })
})

実行結果

eyJpZCI6MSwidXNlcm5hbWUiOiJqaW1teSJ9
{ _tag: 'Right', right: '{"id":1,"username":"jimmy"}' }
{ _tag: 'Right', right: { id: 1, username: 'jimmy' } }
{ _tag: 'Right', right: { id: 1, username: 'jimmy' } }
-----------------

{ _tag: 'Right', right: '' }
Unexpected end of JSON input
Unexpected end of JSON input
-----------------
1
Invalid character: the string to be decoded is not correctly encoded.
Invalid character: the string to be decoded is not correctly encoded.
Invalid character: the string to be decoded is not correctly encoded.
-----------------
eyJpZCI6MSwibmFtZSI6ImppbW15In0=
{ _tag: 'Right', right: '{"id":1,"name":"jimmy"}' }
{ _tag: 'Right', right: { id: 1, name: 'jimmy' } }
{"id":1,"name":"jimmy"} is not User

Chapter 3.4: Either orElse error recovery

E.orElse(onLeft) - 1





import { describe, it } from 'vitest'
import * as E from 'fp-ts/Either'
import { flow, pipe } from 'fp-ts/lib/function'

describe('fp-ts Tutorial', () => {
  it('E.orElse(onLeft) - 1', () => {
    type Email = Readonly<{
      type: 'Email'
      value: string
    }>
    type PhoneNumber = Readonly<{
      type: 'PhoneNumber'
      value: string
    }>

    const emailRegex = /^\S+@\S+\.\S+$/
    const phoneNumberRegex = /^[+]*[(]{0,1}[0-9]{1,4}[)]{0,1}[-\s./0-9]*$/

    const debug_log = <T>(x: T) => {
      console.log(x)
      return x
    }
    const validateEmail = flow(
      E.fromPredicate(
        (mabeEmail: string) => emailRegex.test(mabeEmail),
        (invalidEmail) => (invalidEmail.includes('@') ? 'MalformedEmail' : 'NotAnEmail')
      ),
      E.map(
        (email): Email => ({
          type: 'Email',
          value: email
        })
      )
    )

    const validatePhoneNumber = flow(
      E.fromPredicate(
        (maybePhoneNumber: string) => phoneNumberRegex.test(maybePhoneNumber),
        () => 'InvalidPhoneNumber' as const
      ),
      E.map(
        (phoneNumber): PhoneNumber => ({
          type: 'PhoneNumber',
          value: phoneNumber
        })
      )
    )

    const validateLoginName = (loginName: string) =>
      pipe(
        loginName,
        debug_log,
        validateEmail,
        debug_log,
        E.orElseW((e) => (e === 'NotAnEmail' ? validatePhoneNumber(loginName) : E.left(e)))
      )

    console.log(validateLoginName('user@example.com'))
    console.log('------------')
    console.log(validateLoginName('555 1234567'))
    console.log('------------')
    console.log(validateLoginName('foo@bar'))
    console.log('------------')
    console.log(validateLoginName('foo123'))
  })
})

実行結果

user@example.com
{ _tag: 'Right', right: { type: 'Email', value: 'user@example.com' } }
{ _tag: 'Right', right: { type: 'Email', value: 'user@example.com' } }
------------
555 1234567
{ _tag: 'Left', left: 'NotAnEmail' }
{ _tag: 'Right', right: { type: 'PhoneNumber', value: '555 1234567' } }
------------
foo@bar
{ _tag: 'Left', left: 'MalformedEmail' }
{ _tag: 'Left', left: 'MalformedEmail' }
------------
foo123
{ _tag: 'Left', left: 'NotAnEmail' }
{ _tag: 'Left', left: 'InvalidPhoneNumber' }

E.orElse(onLeft) - 2

エラーを文字列から方に変えたもの

import { describe, it } from 'vitest'
import * as E from 'fp-ts/Either'
import { flow, pipe } from 'fp-ts/lib/function'

describe('fp-ts Tutorial', () => {
  it('E.orElse(onLeft) - 2', () => {
    type Email = Readonly<{
      type: 'Email'
      value: string
    }>
    type PhoneNumber = Readonly<{
      type: 'PhoneNumber'
      value: string
    }>

    type MalformedEmail = Readonly<{
      type: 'MalformedEmail'
      error: Error
    }>
    type NotAnEmail = Readonly<{
      type: 'NotAnEmail'
      error: Error
    }>
    type InvalidPhoneNumber = Readonly<{
      type: 'InvalidPhoneNumber'
      error: Error
    }>

    const emailRegex = /^\S+@\S+\.\S+$/
    const phoneNumberRegex = /^[+]*[(]{0,1}[0-9]{1,4}[)]{0,1}[-\s./0-9]*$/

    const debug_log = <E, A>(x: E.Either<E, A>) => {
      if (E.isLeft(x)) {
        console.log(x.left.error.message)
      } else {
        console.log(x)
      }
      return x
    }
    const validateEmail = flow(
      E.fromPredicate(
        (mabeEmail: string) => emailRegex.test(mabeEmail),
        (invalidEmail): MalformedEmail | NotAnEmail =>
          invalidEmail.includes('@')
            ? { type: 'MalformedEmail', error: Error('Malformed email!') }
            : { type: 'NotAnEmail', error: Error('Not an email!') }
      ),
      E.map(
        (email): Email => ({
          type: 'Email',
          value: email
        })
      )
    )

    const validatePhoneNumber = flow(
      E.fromPredicate(
        (maybePhoneNumber: string) => phoneNumberRegex.test(maybePhoneNumber),
        (): InvalidPhoneNumber => ({
          type: 'InvalidPhoneNumber',
          error: Error('Invalid phone number!')
        })
      ),
      E.map(
        (phoneNumber): PhoneNumber => ({
          type: 'PhoneNumber',
          value: phoneNumber
        })
      )
    )

    const validateLoginName = (loginName: string) =>
      pipe(
        loginName,
        debug_log,
        validateEmail,
        debug_log,
        E.orElseW(
          (e): E.Either<InvalidPhoneNumber | MalformedEmail, PhoneNumber> =>
            e.type === 'NotAnEmail' ? validatePhoneNumber(loginName) : E.left(e)
        )
      )

    console.log(validateLoginName('user@example.com'))
    console.log('------------')
    console.log(validateLoginName('555 1234567'))
    console.log('------------')
    console.log(validateLoginName('foo@bar').left.error.message)
    console.log('------------')
    console.log(validateLoginName('foo123').left.error.message)
  })
})

実行結果

user@example.com
{ _tag: 'Right', right: { type: 'Email', value: 'user@example.com' } }
{ _tag: 'Right', right: { type: 'Email', value: 'user@example.com' } }
------------
555 1234567
Not an email!
{ _tag: 'Right', right: { type: 'PhoneNumber', value: '555 1234567' } }
------------
foo@bar
Malformed email!
Malformed email!
------------
foo123
Not an email!
Invalid phone number!

Chapter 4: IO

具体的なソースコードなし

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0