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
具体的なソースコードなし