타자 스크립트 생성기 남용
94291 단어 functionaltypescript
@effect-ts/core
생태계와 그 일부 데이터 유형, 예를 들어 Effect
,Managed
,Layer
등을 연구했다.우리는 typescript에서 정적 유형 함수 프로그래밍을 사용하는 장점을 상세하게 연구하여 코드의 모듈성, 테스트 가능성과 안전성을 얻었다.
그러나 한 가지 부족한 점이 있다. 우리가 사용하는 API는 비함수식 언어에서 온 사람에게는 좀 어색하다. 함수식 언어에서 온 사람에게는 매우 큰 결함이 있다. 즉, scala
for
와haskelldo
이다.새로 온 사람들에 대해 이 API의 배후 원인을 먼저 탐색해 봅시다.
우리는 이미
Monads
순서 연산의 개념을 어떻게 표시하는지 보았고 우리는 줄곧 chain
로 이 점을 표현하고 있다.이런 프로그래밍 스타일은 함수 조합에 두드러져서 우리는 명령식으로 순서 절차를 표현할 수 있는 방법이 있기를 바란다.
javascript
또는 typescript
에서 온 사람들에게 우리는 async/await
와 유사한 기능을 원하지만 각각Monad
에 보급할 수 있기를 바란다.초기 방법
역사상 typescript의 함수식 프로그래밍 환경에서 비슷한 시도가 많았다
Do
.즉, 우리는 두 가지 방법이 있다.
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 a
는any
이고 어떠한 추리 작업도 없다.우리가 이용하고자 하는 또 다른 특징은
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
의 유형과 일반적인 어떤 유형도 피하고 생성기를 직접 추가하지 않기를 바랍니다.이것도 이런 유형의 차이를 깨뜨릴 것이다.우리는 함수를 사용하여
Effect
를 effect
로 승급할 것이다.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
유형의 효과도 매우 효과적이다.io
나Stream
와 같은 다중렌즈 효과는 문제가 있다. 채택한 방법이 작용하지 않는 효과에 대해 우리가 위의 계산에서 작업을 할 때 우리는 실행하는 모든 단계, 흐름이나 수조에서 교체기가 어떻게 전진하는지 알아차린다. 우리는 많은 요소에 대해 같은 조작을 반복해야 하지만 우리는 하나의 교체기만 있다.교체기는 가변적이기 때문에 복제하거나 어떤 형태의 방어 복제도 할 수 없습니다.
그러나 이것은 종점이 아니다. 만약에 생성기의 주체가
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.ts 즉
Stream
(일회성 상황에 사용되고 매우 효과적)와 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
생태계에서 사용할 수 있는 데이터 유형을 계속 탐색할 것이다!
Reference
이 문제에 관하여(타자 스크립트 생성기 남용), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/matechs/abusing-typescript-generators-4m5h텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)