TS4에서 HKT 인코딩1

128791 단어
TypeScript에서 유효한 HKT 인코딩을 찾으려면 여러 가지 시도가 있습니다.현재 가장 많이 사용되고 가장 믿을 만한 것은 fp-ts이다.
fpts에서 모든 유형은 일련의 유형 레벨 매핑에 기록됩니다. 이러한 매핑 색인은 URI -> Concrete Type이고 매핑 유형마다 다릅니다.
export interface URItoKind<A> {}

export interface URItoKind2<E, A> {}

export interface URItoKind3<R, E, A> {}

export interface URItoKind4<S, R, E, A> {}
이러한 유형의 레벨의 기록은 모듈 확장 기능을 사용하여 점차적으로 채워진다.이 기록에 어떻게 연결되는지 살펴보겠습니다 Either & Option.
export const URI = "Either"

export type URI = typeof URI

declare module "./HKT" {
  interface URItoKind2<E, A> {
    readonly [URI]: Either<E, A>
  }
}
export const URI = "Option"

export type URI = typeof URI

declare module "./HKT" {
  interface URItoKind<A> {
    readonly [URI]: Option<A>
  }
}
향상되면 기록은 다음과 같습니다.
export interface URItoKind<A> {
    Option: Option<A>
    ...
}

export interface URItoKind2<E, A> {
    Either: Either<E, A>
    ...
}

export interface URItoKind3<R, E, A> {
    ReaderTaskEither: ReaderTaskEither<R, E, A>
    ...
}

export interface URItoKind4<S, R, E, A> {
    StateReaderTaskEither: StateReaderTaskEither<S, R, E, A>
    ...
}
이러한 유형에 액세스하려면 올바른 레코드 키를 획득하고 매개 변수를 입력해야 합니다.
type Result = URItoKind<string, number>["Either"]
대응 대상:
Either<string, number>
이 방법을 사용하여 fp ts를 정의합니다.
export type URIS = keyof URItoKind<any>
export type URIS2 = keyof URItoKind2<any, any>
export type URIS3 = keyof URItoKind3<any, any, any>
export type URIS4 = keyof URItoKind4<any, any, any, any>

export type Kind<URI extends URIS, A> = URI extends URIS ? URItoKind<A>[URI] : any
export type Kind2<URI extends URIS2, E, A> = URI extends URIS2
  ? URItoKind2<E, A>[URI]
  : any
export type Kind3<URI extends URIS3, R, E, A> = URI extends URIS3
  ? URItoKind3<R, E, A>[URI]
  : any
export type Kind4<URI extends URIS4, S, R, E, A> = URI extends URIS4
  ? URItoKind4<S, R, E, A>[URI]
  : any
이제 액세스할 수 있습니다.
type Result = Kind2<"Either", string, number>
이러한 구조를 통해 URI가 지정되지 않은 일반 인터페이스를 작성할 수 있습니다.
예를 들어 다음과 같이 쓸 수 있습니다.
export interface Functor1<F extends URIS> {
  readonly URI: F
  readonly map: <A, B>(fa: Kind<F, A>, f: (a: A) => B) => Kind<F, B>
}
또한
const functorOption: Functor1<"Option"> = {
    URI: "Option",
    map: ... // map is now correctly typed to work with Option<*>
}
분명히 이것은 다른 종류를 개괄하기에는 부족하다.fp ts에서는 각 유형의 typeclass (이 문제에서 인터페이스는 공통 URI를 가진다) 에 대해 여러 가지 정의가 있음을 발견할 수 있습니다.
export interface Functor1<F extends URIS> {
  readonly URI: F
  readonly map: <A, B>(fa: Kind<F, A>, f: (a: A) => B) => Kind<F, B>
}

export interface Functor2<F extends URIS2> {
  readonly URI: F
  readonly map: <E, A, B>(fa: Kind2<F, E, A>, f: (a: A) => B) => Kind2<F, E, B>
}

export interface Functor2C<F extends URIS2, E> {
  readonly URI: F
  readonly _E: E
  readonly map: <A, B>(fa: Kind2<F, E, A>, f: (a: A) => B) => Kind2<F, E, B>
}

export interface Functor3<F extends URIS3> {
  readonly URI: F
  readonly map: <R, E, A, B>(fa: Kind3<F, R, E, A>, f: (a: A) => B) => Kind3<F, R, E, B>
}

export interface Functor3C<F extends URIS3, E> {
  readonly URI: F
  readonly _E: E
  readonly map: <R, A, B>(fa: Kind3<F, R, E, A>, f: (a: A) => B) => Kind3<F, R, E, B>
}

export interface Functor4<F extends URIS4> {
  readonly URI: F
  readonly map: <S, R, E, A, B>(
    fa: Kind4<F, S, R, E, A>,
    f: (a: A) => B
  ) => Kind4<F, S, R, E, B>
}
보시다시피 이 네 가지 유형을 제외하고는 *C 인터페이스가 있습니다. E 파라미터에 제약을 추가하는 데 사용됩니다.이것은 Validation에서 사용되는데 그 중에서 EError 채널을 표시하고 우리는 Monoid<E>에 최종적으로 오류를 조합할 것을 요구한다.
이제 이런 유형을 어떻게 사용하는지 봅시다.우리는 어떻게 범형Functor을 사용하는 함수를 작성합니까?
기본적인 상황부터 우리는 일반 함수addOne가 필요하다. 이것은 추가를 통해 디지털 출력을 비추는 것이다.
function addOne<URI extends URIS>(F: Functor1<URI>) {
  return (fa: Kind<F, number>): Kind<F, number> => F.map(fa, (n) => n + 1)
}
적당한 typeclass 실례로 이 함수를 호출하면 데이터 형식에 특정한 함수를 생성합니다.
const addOneOption = addOne(functorOption) // (fa: Option<number>) => Option<number>
우리는 리로드를 통해 다양한 유형을 홍보하고 지원할 수 있다.
function addOne<URI extends URIS4, E>(
  F: Functor4C<URI, E>
): <S, R>(fa: Kind4<F, S, R, E, number>) => Kind4<F, S, R, E, number>
function addOne<URI extends URIS4>(
  F: Functor4<URI>
): <S, R, E>(fa: Kind4<F, S, R, E, number>) => Kind4<F, S, R, E, number>
function addOne<URI extends URIS3, E>(
  F: Functor3C<URI, E>
): <R>(fa: Kind3<F, R, E, number>) => Kind3<F, R, E, number>
function addOne<URI extends URIS3>(
  F: Functor3<URI>
): <R, E>(fa: Kind3<F, R, E, number>) => Kind3<F, R, E, number>
function addOne<URI extends URIS2, E>(
  F: Functor2C<URI, E>
): (fa: Kind2<F, E, number>) => Kind2<F, E, number>
function addOne<URI extends URIS2>(
  F: Functor2<URI>
): <E>(fa: Kind2<F, E, number>) => Kind<F, E, number>
function addOne<URI extends URIS>(
  F: Functor1<URI>
): (fa: Kind<F, number>) => Kind<F, number>
function addOne(F: any) {
  return (fa: any): any => F.map(fa, (n) => n + 1)
}
유일한 번거로움은 매우 무서운 기본 상황 (어떤 것, 어떤 것, 어떤 것) 을 정의하는 것이다.
fp ts에서 다음과 같이 정의할 수 있습니다.
export interface HKT<URI, A> {
  readonly _URI: URI
  readonly _A: A
}

export interface HKT2<URI, E, A> extends HKT<URI, A> {
  readonly _E: E
}

export interface HKT3<URI, R, E, A> extends HKT2<URI, E, A> {
  readonly _R: R
}

export interface HKT4<URI, S, R, E, A> extends HKT3<URI, R, E, A> {
  readonly _S: S
}
현재 우리는 Functor에 특정한 HKT 인터페이스를 정의할 수 있다. 예를 들어 다음과 같다.
export interface Functor<F> {
  readonly URI: F
  readonly map: <A, B>(fa: HKT<F, A>, f: (a: A) => B) => HKT<F, B>
}
이 옵션을 사용하여 기본값을 입력합니다.
function addOne<URI extends URIS4, E>(
  F: Functor4C<URI, E>
): <S, R>(fa: Kind4<F, S, R, E, number>) => Kind4<F, S, R, E, number>
function addOne<URI extends URIS4>(
  F: Functor4<URI>
): <S, R, E>(fa: Kind4<F, S, R, E, number>) => Kind4<F, S, R, E, number>
function addOne<URI extends URIS3, E>(
  F: Functor3C<URI, E>
): <R>(fa: Kind3<F, R, E, number>) => Kind3<F, R, E, number>
function addOne<URI extends URIS3>(
  F: Functor3<URI>
): <R, E>(fa: Kind3<F, R, E, number>) => Kind3<F, R, E, number>
function addOne<URI extends URIS2, E>(
  F: Functor2C<URI, E>
): (fa: Kind2<F, E, number>) => Kind2<F, E, number>
function addOne<URI extends URIS2>(
  F: Functor2<URI>
): <E>(fa: Kind2<F, E, number>) => Kind<F, E, number>
function addOne<URI extends URIS>(
  F: Functor1<URI>
): (fa: Kind<F, number>) => Kind<F, number>
function addOne<URI>(F: Functor<URI>) {
  return (fa: HKT<URI, number>): HKT<URI, number> => F.map(fa, (n) => n + 1)
}
짧고 실용적이지 않습니까?우리는 functors의 작문을 한 편 쓰자.
export interface FunctorComposition<F, G> {
  readonly map: <A, B>(fa: HKT<F, HKT<G, A>>, f: (a: A) => B) => HKT<F, HKT<G, B>>
}

export interface FunctorCompositionHKT1<F, G extends URIS> {
  readonly map: <A, B>(fa: HKT<F, Kind<G, A>>, f: (a: A) => B) => HKT<F, Kind<G, B>>
}

export interface FunctorCompositionHKT2<F, G extends URIS2> {
  readonly map: <E, A, B>(fa: HKT<F, Kind2<G, E, A>>, f: (a: A) => B) => HKT<F, Kind2<G, E, B>>
}

export interface FunctorCompositionHKT2C<F, G extends URIS2, E> {
  readonly map: <A, B>(fa: HKT<F, Kind2<G, E, A>>, f: (a: A) => B) => HKT<F, Kind2<G, E, B>>
}

export interface FunctorComposition11<F extends URIS, G extends URIS> {
  readonly map: <A, B>(fa: Kind<F, Kind<G, A>>, f: (a: A) => B) => Kind<F, Kind<G, B>>
}

export interface FunctorComposition12<F extends URIS, G extends URIS2> {
  readonly map: <E, A, B>(fa: Kind<F, Kind2<G, E, A>>, f: (a: A) => B) => Kind<F, Kind2<G, E, B>>
}

export interface FunctorComposition12C<F extends URIS, G extends URIS2, E> {
  readonly map: <A, B>(fa: Kind<F, Kind2<G, E, A>>, f: (a: A) => B) => Kind<F, Kind2<G, E, B>>
}

export interface FunctorComposition21<F extends URIS2, G extends URIS> {
  readonly map: <E, A, B>(fa: Kind2<F, E, Kind<G, A>>, f: (a: A) => B) => Kind2<F, E, Kind<G, B>>
}

export interface FunctorComposition2C1<F extends URIS2, G extends URIS, E> {
  readonly map: <A, B>(fa: Kind2<F, E, Kind<G, A>>, f: (a: A) => B) => Kind2<F, E, Kind<G, B>>
}

export interface FunctorComposition22<F extends URIS2, G extends URIS2> {
  readonly map: <FE, GE, A, B>(fa: Kind2<F, FE, Kind2<G, GE, A>>, f: (a: A) => B) => Kind2<F, FE, Kind2<G, GE, B>>
}

export interface FunctorComposition22C<F extends URIS2, G extends URIS2, E> {
  readonly map: <FE, A, B>(fa: Kind2<F, FE, Kind2<G, E, A>>, f: (a: A) => B) => Kind2<F, FE, Kind2<G, E, B>>
}

export interface FunctorComposition23<F extends URIS2, G extends URIS3> {
  readonly map: <FE, R, E, A, B>(fa: Kind2<F, FE, Kind3<G, R, E, A>>, f: (a: A) => B) => Kind2<F, FE, Kind3<G, R, E, B>>
}

export interface FunctorComposition23C<F extends URIS2, G extends URIS3, E> {
  readonly map: <FE, R, A, B>(fa: Kind2<F, FE, Kind3<G, R, E, A>>, f: (a: A) => B) => Kind2<F, FE, Kind3<G, R, E, B>>
}
이거 아직 완전하지 않아...
또 다른 제한은 종류마다 독립된 인덱스가 필요하다는 것이다.이것은 typeclass transformers를 매우 비현실적으로 만들었다.
우리의 목표는 더 적은 템플릿 파일로 같은 특성을 얻는 것이다.
이제 @effect-ts/core에서 사용한 인코딩의 제한 버전을 보여 드리겠습니다.@effect-ts/core는 10개의 유형 매개 변수(그중 2개는 인코딩 유니버설 키, 즉 기록된 유니버설 키, 비치는 유니버설 키, 수조의 정수 키 등)를 허용하지만, 간단하게 보기 위해 4개(fp ts의 숫자와 같다)로 제한한다.
전체 코드는 다음 웹 사이트에서 사용할 수 있습니다.
https://github.com/Matechs-Garage/matechs-effect/tree/master/packages/core/src/Prelude/HKT
그리고 TypeClass(영감은 zio prelude):
https://github.com/Matechs-Garage/matechs-effect/tree/master/packages/core/src/Prelude
첫 번째 생각은 여러 개의 기록이 아니라 유형 레벨의 기록 수를 줄이는 것이다. 우리는 단지 하나의 기록만 가지고 있을 뿐이다.
export interface URItoKind<S, R, E, A> {
  XPure: XPure<S, S, R, E, A>
  Effect: Effect<R, E, A>
  Either: Either<E, A>
  Option: Option<A>
}

export type URIS = keyof URItoKind<any, any, any, any>
그리고 우리는 잠시 Kind를 다음과 같이 정의할 수 있다.
export type Kind<F extends URIS, S, R, E, A> = URItoKind<S, R, E, A>[F]
이것은 이미 상당히 많은 템플릿 파일을 삭제했다.그런 다음 TypeClass를 다음과 같이 정의할 수 있습니다.
export interface Functor<F extends URIS> {
  URI: F
  map: <A, A2>(
    f: (a: A) => A2
  ) => <S, R, E>(fa: Kind<F, S, R, E, A>) => Kind<F, S, R, E, A2>
}
및 인스턴스:
export const functorOption: Functor<"Option"> = {
  URI: "Option",
  map: (f) => (fa) => (fa._tag === "None" ? fa : { _tag: "Some", value: f(fa.value) })
}
Bifunctor
export interface Bifunctor<F extends URIS> {
  URI: F
  bimap: <A, A2, E, E2>(
    f: (a: A) => A2,
    g: (a: E) => E2
  ) => <S, R>(fa: Kind<F, S, R, E, A>) => Kind<F, S, R, E2, A2>
}
및 인스턴스:
export const bifunctorEither: Bifunctor<"Either"> = {
  URI: "Either",
  bimap: (f, g) => (fa) =>
    fa._tag === "Left"
      ? { _tag: "Left", left: g(fa.left) }
      : { _tag: "Right", right: f(fa.right) }
}
보기에는 더 좋지만, 우리는 어떻게 제약을 인코딩합니까? 예를 들어 'E' 를 특정 값으로 고정시킵니까?
정답은 구속을 저장하기 위한 일반 C을 추가하는 것입니다.
export type Param = "S" | "R" | "E"

export interface Fix<P extends Param, K> {
  Fix: {
    [p in P]: K
  }
}

export type OrFix<P extends Param, C, V> = C extends Fix<P, infer K> ? K : V

export type Kind<F extends URIS, C, S, R, E, A> = URItoKind<
  OrFix<"S", C, S>,
  OrFix<"R", C, R>,
  OrFix<"E", C, E>,
  A
>[F]
TypeClass를 다음으로 변경합니다.
export interface Functor<F extends URIS, C = {}> {
  URI: F
  map: <A, A2>(
    f: (a: A) => A2
  ) => <S, R, E>(fa: Kind<F, C, S, R, E, A>) => Kind<F, C, S, R, E, A2>
}

export interface Bifunctor<F extends URIS, C = {}> {
  URI: F
  bimap: <A, A2, E, E2>(
    f: (a: A) => A2,
    g: (a: OrFix<"E", C, E>) => OrFix<"E", C, E2>
  ) => <S, R>(fa: Kind<F, C, S, R, E, A>) => Kind<F, C, S, R, E2, A2>
}
인스턴스의 코드는 변경되지 않았지만 이제 다음과 같은 방법으로 구속된 인스턴스를 생성할 수 있습니다.
export const bifunctorStringValidation: Bifunctor<"Either", Fix<"E", string>> = {
  URI: "Either",
  bimap: (f, g) => (fa) =>
    fa._tag === "Left"
      ? { _tag: "Left", left: g(fa.left) }
      : { _tag: "Right", right: f(fa.right) }
}

// <A, A2, E, E2>(f: (a: A) => A2, g: (a: string) => string) => <S, R>(fa: Either<string, A>) => Either<string, A2>
export const bimapValidation = bifunctorStringValidation.bimap
아직 사용하지 않은 인자가 몇 개 있지만, 원하는 서명을 받았습니다.
불행하게도, 여러 개의 등록표와 더 많은 템플릿 파일이 없으면 이러한 환영 유형을 삭제할 수 없지만, 결국, 누가 신경을 쓰겠는가?
하나의 단일한 정의에서 우리는 현재 여러 가지 유형과 여러 가지 잠재적인 제약을 압축할 수 있다. 사실Fix은 교차로에 안전하기 때문에 우리는 작성할 수 있다Fix<"E", string> & Fix<"S", number>.
DelladdOne의 기능은 다음과 같이 향상되었습니다.
function addOne<URI extends URIS, C>(
  F: Functor<URI, C>
): <S, R, E>(fa: Kind<URI, C, S, R, E, number>) => Kind<URI, C, S, R, E, number> {
  return F.map((n) => n + 1)
}
우리는 그것을 여기에 남겨 둘 수 있고, 이미 좋은 저축이 하나 있지만, 단점도 하나 있다.통용 실현 중의 오류는 왕왕 읽을 수 없게 변한다.왜냐하면 URI는 매우 큰 연합체로서 어떤 종류의 오류든 기본적으로 여러 가지 가능한 조합을 시도하여 사용할 수 없는 오류 메시지를 생성하기 때문이다.
우리는 fpts에서 하나의 HKT를 정의하여 기본적인 실현을 작성하고 나머지는 재부팅에 남겨 두지만 HKT에 단독 유형 클래스를 정의하고 싶지 않기 때문에 HKT를 등록표에 추가할 것이다.
export interface F_<A> {
  _tag: "F_"
  A: A
}

export interface URItoKind<S, R, E, A> {
  F_: F_<A>
  XPure: XPure<S, S, R, E, A>
  Effect: Effect<R, E, A>
  Either: Either<E, A>
  Option: Option<A>
}
이제 F 2;를 사용하여 기본을 정의할 수 있습니다.
function addOne<URI extends URIS, C>(
  F: Functor<URI, C>
): <S, R, E>(fa: Kind<URI, C, S, R, E, number>) => Kind<URI, C, S, R, E, number>
function addOne(F: Functor<"F_">): (fa: F_<number>) => F_<number> {
  return F.map((n) => n + 1)
}
이제 구현의 모든 오류 유형이 F 3;에 지정됩니다.
그러나 이러한 "F 3;"솔루션에는 문제가 있습니다.
HKT가 하나면 되는데 우리가 여러 개가 있다면?
예:
export function getFunctorComposition<F, G>(F: Functor<F>, G: Functor<G>): FunctorComposition<F, G> {
  return {
    map: (fa, f) => F.map(fa, (ga) => G.map(ga, f))
  }
}
이를 위해 레지스트리에 "태그"필드를 사용하여 여러 가지 "가짜"유형을 추가합니다.
export interface F_<A> {
  _tag: "F_"
  A: A
}
export interface G_<A> {
  _tag: "G_"
  A: A
}
export interface H_<A> {
  _tag: "H_"
  A: A
}

export interface URItoKind<S, R, E, A> {
  F_: F_<A>
  G_: G_<A>
  H_: H_<A>
  XPure: XPure<S, S, R, E, A>
  Effect: Effect<R, E, A>
  Either: Either<E, A>
  Option: Option<A>
}
이런 방식을 통해 우리는 여러 개의 HKT를 안전하게'명명'하여 혼합할 수 없도록 하고 더 많은 작업을 할 수 있다. 우리는 이러한 논리를 확장하여 다른 형식의 비원시 URI를 받아들여 사용자 정의 파라미터를 삽입하고 HKT를 구분할 수 있다functor-composition-in-core.
충분히 좋아요?아직 접근하지 않았으니 우리는 트랜스포머를 시도해야 한다.
만약 우리가 getFunctorCompositionFunctor를 원한다면?우리는 어떻게 색인을 다시 작성하지 않은 상황에서 이 점을 할 수 있습니까?Either<E, Option<A>>의 URI를 조합하기 위해 우리는 변수 모듈을 사용하여 Kind의 목록을 표시하고 URIS 유형을 귀속적으로 만들 수 있다.
export type KindURI = [URIS, ...URIS[]]

export type Kind<F extends KindURI, C, S, R, E, A> = ((...args: F) => any) extends (
  head: infer H,
  ...rest: infer Rest
) => any
  ? H extends URIS
    ? Rest extends KindURI
      ? URItoKind<
          OrFix<"S", C, S>,
          OrFix<"R", C, R>,
          OrFix<"E", C, E>,
          Kind<Rest, C, S, R, E, A>
        >[H]
      : URItoKind<OrFix<"S", C, S>, OrFix<"R", C, R>, OrFix<"E", C, E>, A>[H]
    : never
  : never
우리의 유형 클래스와 실례는 상응하는 변화가 발생할 것이다.
export interface Functor<F extends KindURI, C = {}> {
  URI: F
  map: <A, A2>(
    f: (a: A) => A2
  ) => <S, R, E>(fa: Kind<F, C, S, R, E, A>) => Kind<F, C, S, R, E, A2>
}

export interface Bifunctor<F extends KindURI, C = {}> {
  URI: F
  bimap: <A, A2, E, E2>(
    f: (a: A) => A2,
    g: (a: OrFix<"E", C, E>) => OrFix<"E", C, E2>
  ) => <S, R>(fa: Kind<F, C, S, R, E, A>) => Kind<F, C, S, R, E2, A2>
}

export const functorOption: Functor<["Option"]> = {
  URI: ["Option"],
  map: (f) => (fa) => (fa._tag === "None" ? fa : { _tag: "Some", value: f(fa.value) })
}

export const bifunctorEither: Bifunctor<["Either"]> = {
  URI: ["Either"],
  bimap: (f, g) => (fa) =>
    fa._tag === "Left"
      ? { _tag: "Left", left: g(fa.left) }
      : { _tag: "Right", right: f(fa.right) }
}

export const bifunctorStringValidation: Bifunctor<["Either"], Fix<"E", string>> = {
  URI: ["Either"],
  bimap: (f, g) => (fa) =>
    fa._tag === "Left"
      ? { _tag: "Left", left: g(fa.left) }
      : { _tag: "Right", right: f(fa.right) }
}

function addOne<URI extends KindURI, C>(
  F: Functor<URI, C>
): <S, R, E>(fa: Kind<URI, C, S, R, E, number>) => Kind<URI, C, S, R, E, number>
function addOne<C>(F: Functor<["F_"], C>): (fa: F_<number>) => F_<number> {
  return F.map((n) => n + 1)
}
하지만 이제 우리는 할 수 있다.
export const functorEitherOption: Functor<["Either", "Option"], {}> = {
  URI: ["Either", "Option"],
  map: (f) => (fa) =>
    fa._tag === "Left"
      ? { _tag: "Left", left: fa.left }
      : fa.right._tag === "None"
      ? { _tag: "Right", right: { _tag: "None" } }
      : { _tag: "Right", right: { _tag: "Some", value: f(fa.right.value) } }
}
다시 인덱스가 없는 상황에서, 우리는 조합 형식의 실례를 만들었다.그것이 유효하다는 것을 증명하기 위해서 우리는 우리의 Kind를 사용할 수 있다.
// <S, R, E>(fa: Either<E, Option<number>>) => Either<E, Option<number>>
const addOneEitherOption = addOne(functorEitherOption)
너무 많은 환영 유형을 제외하고 우리가 원하는 것은 이 서명이다.
또한 addOne 매개 변수를 사용하여 매개 변수의 방차를 인코딩하고 주석 방차와 관련된 일반적인 매개 변수를 혼합하는 실용 프로그램 유형을 정의한다.
코드를 보십시오https://github.com/Matechs-Garage/matechs-effect!
본문 코드 발췌: https://gist.github.com/mikearnaldi/7388dcf4eda013d806858d945c574fbb

좋은 웹페이지 즐겨찾기