타자 스크립트 생성기 남용

94291 단어 functionaltypescript
본 시리즈의 앞의 몇 장에서 우리는 @effect-ts/core 생태계와 그 일부 데이터 유형, 예를 들어 Effect,Managed,Layer 등을 연구했다.
우리는 typescript에서 정적 유형 함수 프로그래밍을 사용하는 장점을 상세하게 연구하여 코드의 모듈성, 테스트 가능성과 안전성을 얻었다.
그러나 한 가지 부족한 점이 있다. 우리가 사용하는 API는 비함수식 언어에서 온 사람에게는 좀 어색하다. 함수식 언어에서 온 사람에게는 매우 큰 결함이 있다. 즉, scalafor와haskelldo이다.
새로 온 사람들에 대해 이 API의 배후 원인을 먼저 탐색해 봅시다.
우리는 이미 Monads 순서 연산의 개념을 어떻게 표시하는지 보았고 우리는 줄곧 chain로 이 점을 표현하고 있다.
이런 프로그래밍 스타일은 함수 조합에 두드러져서 우리는 명령식으로 순서 절차를 표현할 수 있는 방법이 있기를 바란다.javascript 또는 typescript에서 온 사람들에게 우리는 async/await와 유사한 기능을 원하지만 각각Monad에 보급할 수 있기를 바란다.

초기 방법
역사상 typescript의 함수식 프로그래밍 환경에서 비슷한 시도가 많았다Do.
즉, 우리는 두 가지 방법이 있다.
  • Paul Grey의 첫 번째 문장https://paulgray.net/do-syntax-in-typescript/은 fluent 기반api
  • 를 사용하여 상세하게 설명하였다.
  • 두 번째는 Giulio Canti가 파이프 기반api를 사용하는 것이다
  • 두 번째:
    import * as T from "@effect-ts/core/Effect"
    import { pipe } from "@effect-ts/core/Function"
    
    const program = pipe(
      T.do,
      T.bind("a", () => T.succeed(1)),
      T.bind("b", () => T.succeed(2)),
      T.bind("c", ({ a, b }) => T.succeed(a + b)),
      T.map(({ c }) => c)
    )
    
    pipe(
      program,
      T.chain((c) =>
        T.effectTotal(() => {
          console.log(c)
        })
      ),
      T.runMain
    )
    
    기본적으로 여기서 발생한 것은 T.do 빈 상태{}를 초기화하여 계산 범위를 유지하고 bind를 사용하여 변수를 점차적으로 채우는 것이다.
    이러한 방법으로 각 단계는 점 앞에 정의된 전체 변수에 액세스할 수 있습니다.
    fluent와 pipeable API는 모두 매우 사용하기 좋으나, 원생 typescript는 여전히 느껴지지 않으며, 매번 귀속 작업에서 현식 접근 역할 영역을 나타낼 때마다 좋은 중복성을 가진다.

    발전기의 세계로 들어가다
    이 API를 사용하기 전에 "downlevelIteration": true에서 tsconfig.json를 활성화해야 합니다.
    발전기가 무엇인지 탐색해 봅시다.
    function* countTo(n: number) {
      let i = 0
      while (i < n) {
        yield i++
      }
    }
    
    const iterator = countTo(10)
    
    let current = iterator.next()
    
    while (!current.done) {
      console.log(current)
    
      current = iterator.next()
    }
    
    인쇄:
    { value: 0, done: false }
    { value: 1, done: false }
    { value: 2, done: false }
    { value: 3, done: false }
    { value: 4, done: false }
    { value: 5, done: false }
    { value: 6, done: false }
    { value: 7, done: false }
    { value: 8, done: false }
    { value: 9, done: false }
    
    그래서 기본적으로 모든 yield는 우리에게 값을 주고 실행은 호출자가 호출 생성기를 통해 제어한다.
    이제 두 번째 사항을 살펴보겠습니다.
    function* countTo(n: number) {
      let i = 0
      while (i < n) {
        const a = yield i++
    
        console.log(a)
      }
    }
    
    const iterator = countTo(10)
    
    let input = "a"
    let current = iterator.next(input)
    
    while (!current.done) {
      console.log(current)
    
      input += "a"
      current = iterator.next(input)
    }
    
    인쇄:
    { value: 0, done: false }
    aa
    { value: 1, done: false }
    aaa
    { value: 2, done: false }
    aaaa
    { value: 3, done: false }
    aaaaa
    { value: 4, done: false }
    aaaaaa
    { value: 5, done: false }
    aaaaaaa
    { value: 6, done: false }
    aaaaaaaa
    { value: 7, done: false }
    aaaaaaaaa
    { value: 8, done: false }
    aaaaaaaaaa
    { value: 9, done: false }
    aaaaaaaaaaa
    
    따라서 기본적으로 하나의 생성기를 통해 우리는 소비자의 수익률 반환치를 추출하여 변수 범위를 채울 수 있다.
    발전기 주체 중의 유형은 매우 엉망이다const aany이고 어떠한 추리 작업도 없다.
    우리가 이용하고자 하는 또 다른 특징은 yield* 하나의 생성기를 하나의 생성기로 바꾸는 것이다. 놀랍게도 이것은 유형에 있어서 매우 잘 작동한다
    :)
    어디 보자.
    function* constant<A>(a: A): Generator<A, A, A> {
      return yield a
    }
    
    function* countTo(n: number) {
      let i = 0
      while (i < n) {
        const a = yield* constant(i++)
    
        console.log(a)
      }
    }
    
    const iterator = countTo(10)
    
    let current = iterator.next()
    
    while (!current.done) {
      current = iterator.next(current.value)
    }
    
    인쇄:
    0
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    모든 유형이 유효합니다. 우리가 읽은 constant 서명에서 첫 번째 A는 수익률의 유형이고 두 번째는 수익률의 유형이며 세 번째는 다음 단계에 필요한 유형입니다.
    이 아이디어는 하나의 생성기를 사용하여 1원 상하문에서 계산체를 설명한 다음에 실행할 때 생성기에 체인식 계산을 만들도록 하는 것이다. (최초의 시도는 https://github.com/nythrox 와 다른 몇몇 사람들이 완성했다. 모든 내용을 기억하지 못한 것을 용서해 주십시오.)

    발전기 기반Generator<A, A, A>do지금까지 보았던 것들을 한데 모아서 수정Effect의 유형과 일반적인 어떤 유형도 피하고 생성기를 직접 추가하지 않기를 바랍니다.이것도 이런 유형의 차이를 깨뜨릴 것이다.
    우리는 함수를 사용하여 Effecteffect로 승급할 것이다.
    import * as T from "@effect-ts/core/Effect"
    import { pipe } from "@effect-ts/core/Function"
    import type { _E, _R } from "@effect-ts/core/Utils"
    
    export class GenEffect<K, A> {
      constructor(readonly op: K) {}
    
      *[Symbol.iterator](): Generator<GenEffect<K, A>, A, any> {
        return yield this
      }
    }
    
    const adapter = (_: any) => {
      return new GenEffect(_)
    }
    
    export function gen<Eff extends GenEffect<any, any>, AEff>(
      f: (i: {
        <R, E, A>(_: T.Effect<R, E, A>): GenEffect<T.Effect<R, E, A>, A>
      }) => Generator<Eff, AEff, any>
    ): T.Effect<_R<Eff["op"]>, _E<Eff["op"]>, AEff> {
      return T.suspend(() => {
        const iterator = f(adapter as any)
        const state = iterator.next()
    
        function run(
          state: IteratorYieldResult<Eff> | IteratorReturnResult<AEff>
        ): T.Effect<any, any, AEff> {
          if (state.done) {
            return T.succeed(state.value)
          }
          return T.chain_(state.value["op"], (val) => {
            const next = iterator.next(val)
            return run(next)
          })
        }
    
        return run(state)
      })
    }
    
    const program = gen(function* (_) {
      const a = yield* _(T.succeed(1))
      const b = yield* _(T.succeed(2))
    
      return a + b
    })
    
    pipe(
      program,
      T.chain((n) =>
        T.effectTotal(() => {
          console.log(n)
        })
      ),
      T.runMain
    )
    
    자, 좋은 명령식 구도 효과가 생겼습니다!
    내가 배운 것은 "api급에 제한이 있다면 그것을 이용할 기회를 찾아라"는 것이다.
    이 경우, 우리는 필요 없는 generator of effect 함수가 하나 있습니다. 우리는 그것을 사용하여 _effect 로 변환합니다. 우리는 그것을 이용하여 우리가 실현하고자 하는 모든 유형에서 유니버설 자연 변환을 향상시킬 수 있습니다. generator of effects 패키지에서 @effect-ts/core/Effect 함수는 직접 처리할 수 있습니다.
    f: (i: {
        <A>(_: Tag<A>): GenEffect<Has<A>, never, A>
        <E, A>(_: Option<A>, onNone: () => E): GenEffect<unknown, E, A>
        <A>(_: Option<A>): GenEffect<unknown, NoSuchElementException, A>
        <E, A>(_: Either<E, A>): GenEffect<unknown, E, A>
        <R, E, A>(_: Effect<R, E, A>): GenEffect<R, E, A>
        <R, E, A>(_: Managed<R, E, A>): GenEffect<R, E, A>
      }) => Generator<Eff, AEff, any>
    
    그래서 너는 이렇게 그것을 사용할 수 있다.
    import * as E from "@effect-ts/core/Classic/Either"
    import * as O from "@effect-ts/core/Classic/Option"
    import * as T from "@effect-ts/core/Effect"
    import * as M from "@effect-ts/core/Effect/Managed"
    import { pipe } from "@effect-ts/core/Function"
    
    const result = T.gen(function* (_) {
      const a = yield* _(O.some(1))
      const b = yield* _(O.some(2))
      const c = yield* _(E.right(3))
      const d = yield* _(T.access((_: { n: number }) => _.n))
      const e = yield* _(
        M.makeExit_(
          T.effectTotal(() => {
            console.log("open")
    
            return 5
          }),
          () =>
            T.effectTotal(() => {
              console.log("release")
            })
        )
      )
    
      yield* _(
        T.effectTotal(() => {
          console.log(a + b + c + d + e)
        })
      )
    })
    
    pipe(result, T.provideAll({ n: 4 }), T.runMain)
    
    이렇게 하면 다음과 같은 결과가 발생합니다.
    open
    15
    release
    

    다중 렌즈 단자
    지금까지 탐색한 기교는 단일치의 효과가 발생하는 데 매우 효과적이다. 예를 들어gen,Effect와 그 어떤Managed 유형의 효과도 매우 효과적이다.ioStream와 같은 다중렌즈 효과는 문제가 있다. 채택한 방법이 작용하지 않는 효과에 대해 우리가 위의 계산에서 작업을 할 때 우리는 실행하는 모든 단계, 흐름이나 수조에서 교체기가 어떻게 전진하는지 알아차린다. 우리는 많은 요소에 대해 같은 조작을 반복해야 하지만 우리는 하나의 교체기만 있다.
    교체기는 가변적이기 때문에 복제하거나 어떤 형태의 방어 복제도 할 수 없습니다.
    그러나 이것은 종점이 아니다. 만약에 생성기의 주체가Array라고 가정하면 수익률 내부의 코드 줄 외에 외부 변수를 수정하거나 부작용을 수행할 수 있는 코드가 한 줄도 없기 때문에 우리는 계산 창고를 유지하고 매번 교체기https://github.com/mattiamanzati를 리셋해서 이 문제를 해결할 수 있다.
    이런 기술의 성능 복잡도는 O(n^2)로 그 중에서pure는 생산량(생산원소의 수량이 아닌)이기 때문에 대형 발전기를 건조할 때 반드시 이 점을 고려해야 한다.n 생성기의 코드를 살펴보겠습니다.
    import type { Effect } from "../../Effect"
    import { fromEither, service } from "../../Effect"
    import { die } from "../../Effect/die"
    import type { Either } from "../../Either"
    import { NoSuchElementException, PrematureGeneratorExit } from "../../GlobalExceptions"
    import type { Has, Tag } from "../../Has"
    import * as L from "../../List"
    import type { Option } from "../../Option"
    import type { _E, _R } from "../../Utils"
    import { isEither, isOption, isTag } from "../../Utils"
    import { chain_ } from "./chain"
    import { Stream } from "./definitions"
    import { fail } from "./fail"
    import { fromEffect } from "./fromEffect"
    import { succeed } from "./succeed"
    import { suspend } from "./suspend"
    
    export class GenStream<R, E, A> {
      readonly _R!: (_R: R) => void
      readonly _E!: () => E
      readonly _A!: () => A
      constructor(readonly effect: Stream<R, E, A>) {}
      *[Symbol.iterator](): Generator<GenStream<R, E, A>, A, any> {
        return yield this
      }
    }
    
    const adapter = (_: any, __?: any) => {
      if (isOption(_)) {
        return new GenStream(
          _._tag === "None"
            ? fail(__ ? __() : new NoSuchElementException())
            : succeed(_.value)
        )
      } else if (isEither(_)) {
        return new GenStream(fromEffect(fromEither(() => _)))
      } else if (_ instanceof Stream) {
        return new GenStream(_)
      } else if (isTag(_)) {
        return new GenStream(fromEffect(service(_)))
      }
      return new GenStream(fromEffect(_))
    }
    
    export function gen<RBase, EBase, AEff>(): <Eff extends GenStream<RBase, EBase, any>>(
      f: (i: {
        <A>(_: Tag<A>): GenStream<Has<A>, never, A>
        <E, A>(_: Option<A>, onNone: () => E): GenStream<unknown, E, A>
        <A>(_: Option<A>): GenStream<unknown, NoSuchElementException, A>
        <E, A>(_: Either<E, A>): GenStream<unknown, E, A>
        <R, E, A>(_: Effect<R, E, A>): GenStream<R, E, A>
        <R, E, A>(_: Stream<R, E, A>): GenStream<R, E, A>
      }) => Generator<Eff, AEff, any>
    ) => Stream<_R<Eff>, _E<Eff>, AEff>
    export function gen<EBase, AEff>(): <Eff extends GenStream<any, EBase, any>>(
      f: (i: {
        <A>(_: Tag<A>): GenStream<Has<A>, never, A>
        <E, A>(_: Option<A>, onNone: () => E): GenStream<unknown, E, A>
        <A>(_: Option<A>): GenStream<unknown, NoSuchElementException, A>
        <E, A>(_: Either<E, A>): GenStream<unknown, E, A>
        <R, E, A>(_: Effect<R, E, A>): GenStream<R, E, A>
        <R, E, A>(_: Stream<R, E, A>): GenStream<R, E, A>
      }) => Generator<Eff, AEff, any>
    ) => Stream<_R<Eff>, _E<Eff>, AEff>
    export function gen<AEff>(): <Eff extends GenStream<any, any, any>>(
      f: (i: {
        <A>(_: Tag<A>): GenStream<Has<A>, never, A>
        <E, A>(_: Option<A>, onNone: () => E): GenStream<unknown, E, A>
        <A>(_: Option<A>): GenStream<unknown, NoSuchElementException, A>
        <E, A>(_: Either<E, A>): GenStream<unknown, E, A>
        <R, E, A>(_: Effect<R, E, A>): GenStream<R, E, A>
        <R, E, A>(_: Stream<R, E, A>): GenStream<R, E, A>
      }) => Generator<Eff, AEff, any>
    ) => Stream<_R<Eff>, _E<Eff>, AEff>
    export function gen<Eff extends GenStream<any, any, any>, AEff>(
      f: (i: {
        <A>(_: Tag<A>): GenStream<Has<A>, never, A>
        <E, A>(_: Option<A>, onNone: () => E): GenStream<unknown, E, A>
        <A>(_: Option<A>): GenStream<unknown, NoSuchElementException, A>
        <E, A>(_: Either<E, A>): GenStream<unknown, E, A>
        <R, E, A>(_: Effect<R, E, A>): GenStream<R, E, A>
        <R, E, A>(_: Stream<R, E, A>): GenStream<R, E, A>
      }) => Generator<Eff, AEff, any>
    ): Stream<_R<Eff>, _E<Eff>, AEff>
    export function gen(...args: any[]): any {
      function gen_<Eff extends GenStream<any, any, any>, AEff>(
        f: (i: any) => Generator<Eff, AEff, any>
      ): Stream<_R<Eff>, _E<Eff>, AEff> {
        return suspend(() => {
          function run(replayStack: L.List<any>): Stream<any, any, AEff> {
            const iterator = f(adapter as any)
            let state = iterator.next()
            for (const a of replayStack) {
              if (state.done) {
                return fromEffect(die(new PrematureGeneratorExit()))
              }
              state = iterator.next(a)
            }
            if (state.done) {
              return succeed(state.value)
            }
            return chain_(state.value["effect"], (val) => {
              return run(L.append_(replayStack, val))
            })
          }
          return run(L.empty())
        })
      }
    
      if (args.length === 0) {
        return (f: any) => gen_(f)
      }
      return gen_(args[0])
    }
    
    부터https://github.com/Matechs-Garage/matechs-effect/blob/master/packages/system/src/Stream/Stream/gen.ts
    다른 단일 값을 지원하는 모든 어댑터 코드를 다시 로드하고 지원하는 경우를 제외하고 중요한 부분은 다음과 같습니다.
    suspend(() => {
      function run(replayStack: L.List<any>): Stream<any, any, AEff> {
        const iterator = f(adapter as any)
        let state = iterator.next()
        for (const a of replayStack) {
          if (state.done) {
            return fromEffect(die(new PrematureGeneratorExit()))
          }
          state = iterator.next(a)
        }
        if (state.done) {
          return succeed(state.value)
        }
        return chain_(state.value["effect"], (val) => {
          return run(L.append_(replayStack, val))
        })
      }
      return run(L.empty())
    })
    
    보시다시피, 우리는 결과를 한 무더기 가지고 있으며, 매번 호출할 때마다 국부 범위를 재구성했다.
    이것은 이전과 같이 사용할 수 있다.
    import * as E from "@effect-ts/core/Classic/Either"
    import * as O from "@effect-ts/core/Classic/Option"
    import * as T from "@effect-ts/core/Effect"
    import * as S from "@effect-ts/core/Effect/Stream"
    import { pipe } from "@effect-ts/core/Function"
    
    const result = S.gen(function* (_) {
      const a = yield* _(O.some(0))
      const b = yield* _(E.right(1))
      const c = yield* _(T.succeed(2))
      const d = yield* _(S.fromArray([a, b, c]))
    
      return d
    })
    
    pipe(
      result,
      S.runCollect,
      T.chain((res) =>
        T.effectTotal(() => {
          console.log(res)
        })
      ),
      T.runMain
    )
    
    다음이 발생합니다.
    [0, 1, 2]
    

    개괄하다
    HKT 구조와 앞의 몇 장에서 기술한 전주 덕분에 우리는 이러한 방법을 모든 리스트에 보급할 수 있다. 유니버설 코드는 두 가지 사용 가능한 함수가 있다. https://github.com/Matechs-Garage/matechs-effect/blob/master/packages/core/src/Prelude/DSL/gen.tsStream(일회성 상황에 사용되고 매우 효과적)와 genF(여러 번 상황에 사용되며 n^2의 복잡도는 n회 생산량이다).

    보너스:
    지금까지 본 모든 내용을 사용하여 2개의 서비스를 사용하는 간단한 프로그램의 시뮬레이션을 구축합니다. 시뮬레이션이 완성될 때 메시지의 메시지 에이전트와 상태를 유지하는 데이터베이스를 새로 고칩니다.
    import "@effect-ts/core/Operators"
    
    import * as Array from "@effect-ts/core/Classic/Array"
    import * as Map from "@effect-ts/core/Classic/Map"
    import * as T from "@effect-ts/core/Effect"
    import * as L from "@effect-ts/core/Effect/Layer"
    import * as M from "@effect-ts/core/Effect/Managed"
    import * as Ref from "@effect-ts/core/Effect/Ref"
    import type { _A } from "@effect-ts/core/Utils"
    import { tag } from "@effect-ts/system/Has"
    
    // make Database Live
    export const makeDbLive = M.gen(function* (_) {
      const ref = yield* _(
        Ref.makeRef<Map.Map<string, string>>(Map.empty)["|>"](
          M.make((ref) => ref.set(Map.empty))
        )
      )
    
      return {
        get: (k: string) => ref.get["|>"](T.map(Map.lookup(k)))["|>"](T.chain(T.getOrFail)),
        put: (k: string, v: string) => ref["|>"](Ref.update(Map.insert(k, v)))
      }
    })
    
    // simulate a database connection to a key-value store
    export interface DbConnection extends _A<typeof makeDbLive> {}
    
    // Tag<DbConnection>
    export const DbConnection = tag<DbConnection>()
    
    // Database Live Layer
    export const DbLive = L.fromManaged(DbConnection)(makeDbLive)
    
    // make Broker Live
    export const makeBrokerLive = M.gen(function* (_) {
      const ref = yield* _(
        Ref.makeRef<Array.Array<string>>(Array.empty)["|>"](
          M.make((ref) =>
            ref.get["|>"](
              T.chain((messages) =>
                T.effectTotal(() => {
                  console.log(`Flush:`)
                  messages.forEach((message) => {
                    console.log("- " + message)
                  })
                })
              )
            )
          )
        )
      )
    
      return {
        send: (message: string) => ref["|>"](Ref.update(Array.snoc(message)))
      }
    })
    
    // simulate a connection to a message broker
    export interface BrokerConnection extends _A<typeof makeBrokerLive> {}
    
    // Tag<BrokerConnection>
    export const BrokerConnection = tag<BrokerConnection>()
    
    // Broker Live Layer
    export const BrokerLive = L.fromManaged(BrokerConnection)(makeBrokerLive)
    
    // Main Live Layer
    export const ProgramLive = L.all(DbLive, BrokerLive)
    
    // Program Entry
    export const main = T.gen(function* (_) {
      const { get, put } = yield* _(DbConnection)
      const { send } = yield* _(BrokerConnection)
    
      yield* _(put("ka", "a"))
      yield* _(put("kb", "b"))
      yield* _(put("kc", "c"))
    
      const a = yield* _(get("ka"))
      const b = yield* _(get("kb"))
      const c = yield* _(get("kc"))
    
      const s = `${a}-${b}-${c}`
    
      yield* _(send(s))
    
      return s
    })
    
    // run the program and print the output
    main["|>"](T.provideSomeLayer(ProgramLive))["|>"](T.runMain)
    
    이러한 Builder 기반 접근 방식을 통해 서비스에 액세스하는 것이 얼마나 쉬운지 알 수 있습니다.
    const { get, put } = yield* _(DbConnection)
    
    직접genWithHistoryF 서비스yielding는 귀하가 그 내용에 접근할 수 있도록 합니다.
    또한 코드에 돋보이게 하면, 완전한 유형이 정확하게 추정되고, 거의 명시적으로 지정된 적이 없다는 것을 알 수 있다.

    기대 많이 해주세요.
    이 시리즈의 다음 글에서 2주 후에 우리는 Tag 생태계에서 사용할 수 있는 데이터 유형을 계속 탐색할 것이다!

    좋은 웹페이지 즐겨찾기