Fp-ts 실용 안내서: P4-수조, 반군, 반군

소개


실용적인 방법을 배우는 네 번째 문장에 오신 것을 환영합니다.이 시리즈는 fp-ts와 함수 프로그래밍을 배우는 데 주력하고 수학 지식이 필요하지 않습니다.
이 글에서 나는 fp-ts에서 수조를 효과적으로 사용하는 방법과 semigroupsmonoids를 어떻게 수조에서 쉽게 조합하여 조작하는지 소개할 것이다.

스토리지 기반 지식


매일 만나는 가장 기본적인 데이터 구조는 그룹 데이터 형식입니다.수조는 우리에게 매우 유용하다. 왜냐하면 우리는 그것들을 사용하여 그 원소를 두루 훑어보면서 계산을 실행할 수 있기 때문이다.그러나 우리가 fp-ts를 사용하여 훑어보기 전에 기초부터 순환으로 돌아가자.

순환


반복 수조의 전통적인 방법은 색인 변수i가 있는 순환을 사용하는 것이다.
const foo = [1, 2, 3]

let sum = 0
for (let i = 0; i < foo.length; i++) {
  sum += foo[i]
}
console.log(sum) // 6

For of 순환


우리도 색인 계수기를 사용하지 않고 문법 직접 교체 요소를 사용할 수 있다.
const foo = [1, 2, 3]

let sum = 0
for (const x of foo) {
  sum += x
}
console.log(sum) // 6

기능 회로


하지만 솔직히 요즘 모든 쿨한 아이들은 특이한 함수for-of, map, reduce 문법을 사용하고 있다.
const foo = [1, 2, 3, 4, 5]

const sum = foo
  .map((x) => x - 1)
  .filter((x) => x % 2 === 0)
  .reduce((prev, next) => prev + next, 0)
console.log(sum) // 6
fp-ts에서 우리는 같은 일을 할 수 있지만 문법은 다르다.
import * as A from 'fp-ts/lib/Array'
import { pipe } from 'fp-ts/lib/function'

const foo = [1, 2, 3, 4, 5]

const sum = pipe(
  A.array.map(foo, (x) => x - 1),
  A.filter((x) => x % 2 === 0),
  A.reduce(0, (prev, next) => prev + next),
)
console.log(sum) // 6
하지만 문법사탕처럼 보이기 때문에 주제에서 벗어나자.정규 그룹 방법만 사용할 수 있을 때 fp-ts를 왜 설치합니까?

스토리지 확장


만약 당신이 Python 세계에서 왔다면, Typescript에 내장된 수조 방법이 없다는 것을 놀라게 될 것이다. 예를 들어 filter .기이한 진열 작업에 필요한 추가 배터리를 제공하기 때문에 fp-ts를 사용해야 합니다.
import * as A from 'fp-ts/lib/Array'
import { pipe } from 'fp-ts/lib/function'

const foo = [1, 2, 3]
const bar = ['a', 'b', 'c']

const zipped = pipe(foo, A.zip(bar))
console.log(zipped) // [[1, 'a], [2, 'b], [3, 'c']]
이 점에 대해 가장 멋있는 것은 그룹의 원소의 유형 안전성을 보존하고 있다는 것이다.zip의 유형은 zipped 이 아니라 Array<[number, string]> 이다.다시 말하면 Array<Array<number | string>>tuples 의 수조이다.
만약 당신이 이것이 매우 멋있다고 생각한다면, 당신은 unzip, partition, flatten 연산자에 대해서도 흥미를 느낄 수 있습니다.

어레이 보안


Typescript에서 유형이 안전하지 않은 일 중 하나는 그룹 접근과 돌연변이입니다.Java World에서 온 경우 Typescript에 이상이 없음을 알아야 합니다.경계를 초과한 요소에 접근하여 되돌려줍니다zipped.그룹 경계 밖에서 그룹을 변이할 때 정의되지 않은 행동을 만들 수도 있습니다.
const foo = [1, 2, 3]

const x: number = foo[4] // no compile error
foo[5] = 2 // no runtime error
console.log(foo) // [1, 2, 3, undefined, undefined, 2]
지금, 이 코드 단편만 보면, 나는 당신들에게 우리가 확실히 무서운 시대에 살고 있다는 것을 보증할 수 있습니다.그러나 터널 끝에 빛이 있다.유형 보안 어레이 액세스가 임박했습니다Typescript 4.1.그들이 사용하는 용어는 itpedantic index signatures이다.
또한 우리가 순수함에 대한 끝없는 추구에서 fpts가 우리를 IndexOutOfBounds의 교수대에서 구해낼 것을 기대해야 한다.

찾다


만약 우리가 수조의 원소에 접근할 때 형식을 안전하게 유지하고 싶다면, lookup 함수를 사용해야 한다.그것은 두 개의 인자, 한 개의 인덱스와 한 개의 그룹을 받아들여서 undefined 형식으로 되돌려줍니다.
import * as A from 'fp-ts/lib/Array'
import { pipe } from 'fp-ts/lib/function'

pipe([1, 2, 3], A.lookup(1)) // { _tag: 'Some', value: 2 }
pipe([1, 2, 3], A.lookup(3)) // { _tag: 'None' }
옵션이 되돌아오기 때문에, 우리는 가능한 undefined 사례를 처리해야 한다.
그러나 우리가 첫 번째 원소(수조 머리라고도 부른다)에 흥미를 느끼고 수조가 비어 있지 않다는 것을 알게 되면 이것은 매우 번거로워질 수 있다.
const foo = [1, 2, 3]
if (foo.length > 0) {
  // We don't want an Option since we know it will always be some
  const firstElement = A.head(foo) // { _tag: 'Some', value: 1 }
}
이 문제를 해결하기 위해 fpts는 또 다른 유형 Option 이 있다.none 유형에도 NonEmptyArray 연산자가 하나 있지만, 기본 값이 아니라 NonEmptyArray 로 되돌아옵니다.
유형 정의를 확인하여 차이를 확인합니다.
// Array
export declare const head: <A>(as: A[]) => Option<A>

// NonEmptyArray
export declare const head: <A>(nea: NonEmptyArray<A>) => A
지금 우리는 이 문제에 대답해야 한다. 우리는 어떻게 head부터 시작해야 하는가.🠂 Option .
우리의 예시에서 우리는 Array<A> 문장을 NonEmptyArray<A> 보호로 바꿀 수 있다.
// Fp-ts Type guard
export declare const isNonEmpty: <A>(as: A[]) => as is NonEmptyArray<A>
import * as A from 'fp-ts/lib/Array'
import * as NEA from 'fp-ts/lib/NonEmptyArray'

const foo = [1, 2, 3]
if (A.isNonEmpty(foo)) {
  const firstElement = NEA.head(foo) // 1
}
봐라, 우리가 얻은 것은 값이지 if 이 아니다.
간단히 말하면, 형식의 안전에 진정으로 관심이 있다면, 검색을 사용하여 그룹의 요소에 접근하십시오.
insertAtupdateAt 연산자도 확인하십시오.

파티션 비동질 패턴


Typescript에서 우리는 두 가지 유형의 수조를 가질 수 있다. 그것이 바로 동질과 비동질이다.동질수 그룹에서 모든 원소는 반드시 같은 유형이어야 한다.비동질수 그룹의 원소는 유형을 병합할 수 있다.
// Homogenous
const foo = [1, 2, 3] // number[]

// Non Homogenous
const bar = [1, '2', 3] // (string | number)[]
우리가 비동질수 그룹을 교체할 때, 우리는 유형 병집 간의 공통점에 대한 제한을 받는다.
type Foo = {
  readonly _tag: 'Foo'
  readonly f: () => number
}

type Bar = {
  readonly _tag: 'Bar'
  readonly g: () => number
}

declare const arr: Array<Foo | Bar>
for (const a of arr) {
  console.log(a._tag) // Ok
  console.log(a.f()) // Error: not assignable to Bar
  console.log(a.g()) // Error: not assignable to Foo
}
그러나 우리가 이 문제 영역을 가지고 있다고 가정해 보자.우리는 isNonEmpty 에 속하는 모든 원소의 합을 구하고 Option 에 속하는 모든 원소의 최대치를 곱하고 싶다.
간단하게 말하면, 우리는 그것을 강제로 계산할 수 있다.
function compute(arr: Array<Foo | Bar>) {
  let sum = 0
  let max = Number.NEGATIVE_INFINITY

  arr.forEach((a) => {
    switch (a._tag) {
      case 'Foo':
        sum += a.f()
        break
      case 'Bar':
        max = Math.max(max, a.g())
        break
    }
  })

  return sum * max
}
이거 대박이다.그것은 임무를 완수했다.지루하지도 않아요.그것은 fpts에서 보기에 어떻습니까?
import * as A from 'fp-ts/lib/Array'
import * as NEA from 'fp-ts/lib/NonEmptyArray'
import * as O from 'fp-ts/lib/Option'
import * as E from 'fp-ts/lib/Either'
import { pipe } from 'fp-ts/lib/function'

const compute = (arr: Array<Foo | Bar>) =>
  pipe(
    A.array.partitionMap(arr, (a) =>
      a._tag === 'Foo' ? E.left(a) : E.right(a),
    ),
    ({ left: foos, right: bars }) => {
      const sum = A.array.reduce(foos, 0, (prev, foo) => prev + foo.f())
      const max = A.array.reduce(bars, Number.NEGATIVE_INFINITY, (max, bar) =>
        Math.max(max, bar.g()),
      )

      return sum * max
    },
  )
너는 이것이 보기에 매우 보기 싫고 지루하다고 생각할지도 모른다.네가 옳다.그러나 함수 코드가 더 간결해야 하지 않겠는가?문제는 파이프 코드라고 불리는 코드를 많이 작성해서 총계와 최대치를 단일 수치에 써야 한다는 것이다.
이 문제를 해결하기 위해서 우리는 반군과 반군을 배워야 한다.

반떼


반군은 두 개의 차례 값을 받아들일 수 있고 하나의 차례 값을 생성할 수 있는 유형이다.반군은 Foo라고 불리는 함수를 실현해야 한다.concat 작업의 한 예는 덧셈입니다. 두 개의 숫자를 지정하면 덧셈을 통해 그것들을 연결시켜 하나의 숫자를 생성할 수 있습니다.
반군의 유형 정의는 다음과 같다.1
export interface Semigroup<A> {
  readonly concat: (x: A, y: A) => A
}
이것은 우리에게 매우 유용하다. 왜냐하면 우리는 그것을 사용하여 Reduce 함수에서 가산 연산 f() 을 보급할 수 있기 때문이다.
마찬가지로 주의해야 할 것은 반군을 실현할 때 원소 x와 y를 한 글자씩 연결할 필요가 없고 숫자만 생성할 수 있다는 것이다.예를 들어, 너는 항상 한 무리로 돌아갈 수 있다.
const onlyOne: Semigroup<number> = {
  concat: (_, _) => 1,
}
이것 또한 우리가 반군이 구하는 Bar의 최대치를 실현할 수 있다는 것을 의미한다.
const semigroupMax: Semigroup<number> = {
  concat: (x, y) => Math.max(x, y),
}
아니면 더 간결하게 말해.
const semigroupMax: Semigroup<number> = {
  concat: Math.max,
}
우리의 최초의 문제 영역으로 돌아가면, 우리는 이 범례가 어떻게 화합과 최대치 연산에 유용한지 볼 수 있다.하지만 우리에게는 문제가 하나 있다.이 함수는 두 개의 요소를 포함한다.만약 입력 수조가 비어 있다면, 무슨 일이 일어날까요?
이 문제를 해결하기 위해서 우리는 반군을 배워야 한다.

특이점


반군은 반군의 확장이지만, 빈 요소나 기본 요소도 포함한다.
fp-ts에서 이것은 유형 정의의 모습이다
export interface Monoid<A> extends Semigroup<A> {
  readonly empty: A
} 
앞에서 작성한 g() 함수로 돌아가면 reduce 을 덧셈의 빈 요소로 지정하고 concatprev + foo.f() 로 지정합니다.우리는 반군을 위해 같은 값을 삽입할 것이다.
예를 들어, 확장 bars 을 통해 reduce 을 만들 수 있습니다.
import { Monoid } from 'fp-ts/lib/Monoid'

const monoidMax: Monoid<number> = {
  concat: semigroupMax.concat,
  empty: Number.NEGATIVE_INFINITY,
}
현재 우리는 0 함수를 Number.NEGATIVE_INFINITY 함수로 바꾸어 반군으로 줄어드는 책임을 경감시킬 수 있다.2
import { Monoid, monoidSum } from 'fp-ts/lib/Monoid'

const compute = (arr: Array<Foo | Bar>) =>
  pipe(
    A.array.partitionMap(arr, (a) =>
      a._tag === 'Foo' ? E.left(a) : E.right(a),
    ),
    ({ left: foos, right: bars }) => {
      const sum = A.array.foldMap(monoidSum)(foos, (foo) => foo.f())
      const max = A.array.foldMap(monoidMax)(bars, (bar) => bar.g())

      return sum * max
    }, 
  ) 
야, 다행이다!그것은 그렇게 지루하지 않을 뿐만 아니라, 이 함수가 지금 무엇을 하고 있는지 잘 알고 있다.우리들은 우리의 용례를 확장하고 교환 조작의 조건을 가지고 있다.즉, Math.maxonmonoidMaxsemigroupMaxonreduce이다.
반군이 생기면 우리는 higher order function 을 만들고 원하는 행동을 동적으로 만들 수 있다.
const compute = (fooMonoid: Monoid<number>, barMonoid: Monoid<number>) => (
  arr: Array<Foo | Bar>,
) =>
  pipe(
    A.array.partitionMap(arr, (a) =>
      a._tag === 'Foo' ? E.left(a) : E.right(a),
    ),
    ({ left: foos, right: bars }) => {
      const sum = A.array.foldMap(fooMonoid)(foos, (foo) => foo.f())
      const max = A.array.foldMap(barMonoid)(bars, (bar) => bar.g())

      return sum * max
    },
  )

declare const i: number
if (i % 2 === 0) {
  compute(monoidSum, monoidMax)
} else {
  compute(monoidMax, monoidSum)
}
그러나 그것은 이렇게 보일 수도 있다.
function compute(arr: Array<Foo | Bar>, invert: boolean) {
  let sum = 0
  let max = Number.NEGATIVE_INFINITY

  arr.forEach((a) => {
    switch (a._tag) {
      case 'Foo':
        if (invert) {
          max = Math.max(max, a.f())
        } else {
          sum += a.f()
        }
        break
      case 'Bar':
        if (invert) {
          sum += a.g()
        } else {
          max = Math.max(max, a.g())
        }
        break
    }
  })

  return sum * max
}
여기에서 당신은 cyclomatic complexity이 증가함에 따라 명령식 방법이 더욱 나빠졌다는 것을 이해할 수 있습니다.함수식 프로그래밍에서는 조합성이 강하기 때문에 사용자가 프로그램의 행동을 쉽게 변경할 수 있고 코드에 추가적인 복잡성이 생기지 않는다.
이 글은 여기까지입니다.평소와 같이 만약 당신이 이런 내용을 좋아한다면 저에게 따라가서 DM이나 이메일을 보내주세요. 다음을 보고 싶어요!
실제로 반군은 암장을 계승하고 암장은 반군의 보급이다.차분반군은 associative 로 계산된다.SemigroupMagma의 fpts 정의를 참조하십시오. 
내가 가져온 것을 주의해라. foldMap 그것은 단지 덧셈에 쓰이는 반군일 뿐이다. 

좋은 웹페이지 즐겨찾기