Code Monkey home page Code Monkey logo

Comments (14)

gcanti avatar gcanti commented on July 19, 2024 1

@patroza yeah, you are right, moving the key to 'from' makes it much clearer

b: S.PropertySignature<":", number, "c", ":", string, never>;

it's like

b: number <- c: string

Updated example

import * as S from "@effect/schema/Schema"

/*
const schema: S.struct<{
    a: S.Schema<string, string, never>;
    b: S.PropertySignature<":", number, "c", ":", string, never>;
}>

i.e.

S.Schema<{
    readonly a: string;
    readonly b: number;
}, {
    readonly a: string;
    readonly c: string;
}, never>

*/
const schema = S.struct({
  a: S.string,
  b: S.propertySignatureDeclaration(S.NumberFromString).pipe(S.propertySignatureKey("c"))
})

console.log(S.decodeSync(schema)({ a: "a", c: "1" })) // => { a: 'a', b: 1 }

/*
const schema2: S.struct<{
    d: S.Schema<boolean, boolean, never>;
    a: S.Schema<string, string, never>;
    b: S.PropertySignature<":", number, "c", ":", string, never>;
}>

i.e.

S.Schema<{
    readonly a: string;
    readonly b: number;
    readonly d: boolean;
}, {
    readonly a: string;
    readonly c: string;
    readonly d: boolean;
}, never>

*/
const schema2 = S.struct({
  ...schema.fields,
  d: S.boolean
})

console.log(S.decodeSync(schema2)({ a: "a", c: "1", d: true })) // => { d: true, a: 'a', b: 1 }

from effect.

gcanti avatar gcanti commented on July 19, 2024

There is the risk that the .d.ts files explode, I had some issues in the early versions of io-ts

import * as S from "@effect/schema/Schema"
import type { Simplify } from "effect/Types"

declare const struct: <Fields extends S.StructFields>(
  fields: Fields
) =>
  & S.Schema<S.Schema.Context<Fields[keyof Fields]>, Simplify<S.FromStruct<Fields>>, Simplify<S.ToStruct<Fields>>>
  & { readonly fields: Fields }

const schema1 = struct({ a: S.string, b: S.number })
const schema2 = struct({ c: schema1 })
const schema3 = struct({ d: schema2, e: schema1 })
/*
const schema3: S.Schema<never, {
    readonly d: {
        readonly c: {
            readonly a: string;
            readonly b: number;
        };
    };
    readonly e: {
        readonly a: string;
        readonly b: number;
    };
}, {
    readonly d: {
        readonly c: {
            readonly a: string;
            readonly b: number;
        };
    };
    readonly e: {
        readonly a: string;
        readonly b: number;
    };
}> & {
    readonly fields: {
        d: S.Schema<never, {
            readonly c: {
                readonly a: string;
                readonly b: number;
            };
        }, {
            readonly c: {
                readonly a: string;
                readonly b: number;
            };
        }> & {
            readonly fields: {
                c: S.Schema<never, {
                    readonly a: string;
                    readonly b: number;
                }, {
                    readonly a: string;
                    readonly b: number;
                }> & {
                    readonly fields: {
                        a: S.Schema<never, string, string>;
                        b: S.Schema<never, number, number>;
                    };
                };
            };
        };
        e: S.Schema<never, {
            readonly a: string;
            readonly b: number;
        }, {
            readonly a: string;
            readonly b: number;
        }> & {
            readonly fields: {
                a: S.Schema<never, string, string>;
                b: S.Schema<never, number, number>;
            };
        };
    };
}
*/

from effect.

patroza avatar patroza commented on July 19, 2024

There is the risk that the .d.ts files explode, I had some issues in the early versions of io-ts

that's however a risk with any kind of non-opaque struct schema already, and a reason why it's important for people to embrace opaque types.

that said, I would also be fine with exposing a combinator who extract fields instead, but we will loose PropertyDescriptors etc.

from effect.

gcanti avatar gcanti commented on July 19, 2024

Well, the amount of information that a schema carries around definitely matters. The issue here is that with this modification, the size of the .d.ts file grows quadratically as the nesting levels increase

import * as S from "@effect/schema/Schema"
import type { Simplify } from "effect/Types"

declare const struct: <Fields extends S.StructFields>(
  fields: Fields
) => S.Schema<S.Schema.Context<Fields[keyof Fields]>, Simplify<S.FromStruct<Fields>>, Simplify<S.ToStruct<Fields>>> & {
  readonly fields: Fields
}

const schema1 = struct({ a: S.string })
const schema2 = struct({ b: schema1 })
const schema3 = struct({ c: schema2 })
const schema4 = struct({ d: schema3 })
const schema5 = struct({ e: schema4 })
export const schema6 = struct({ e: schema5 }) // 6 levels

.d.ts

import * as S from "@effect/schema/Schema";
export declare const schema6: S.Schema<never, {
    readonly e: {
        readonly e: {
            readonly d: {
                readonly c: {
                    readonly b: {
                        readonly a: string;
                    };
                };
            };
        };
    };
}, {
    readonly e: {
        readonly e: {
            readonly d: {
                readonly c: {
                    readonly b: {
                        readonly a: string;
                    };
                };
            };
        };
    };
}> & {
    readonly fields: {
        e: S.Schema<never, {
            readonly e: {
                readonly d: {
                    readonly c: {
                        readonly b: {
                            readonly a: string;
                        };
                    };
                };
            };
        }, {
            readonly e: {
                readonly d: {
                    readonly c: {
                        readonly b: {
                            readonly a: string;
                        };
                    };
                };
            };
        }> & {
            readonly fields: {
                e: S.Schema<never, {
                    readonly d: {
                        readonly c: {
                            readonly b: {
                                readonly a: string;
                            };
                        };
                    };
                }, {
                    readonly d: {
                        readonly c: {
                            readonly b: {
                                readonly a: string;
                            };
                        };
                    };
                }> & {
                    readonly fields: {
                        d: S.Schema<never, {
                            readonly c: {
                                readonly b: {
                                    readonly a: string;
                                };
                            };
                        }, {
                            readonly c: {
                                readonly b: {
                                    readonly a: string;
                                };
                            };
                        }> & {
                            readonly fields: {
                                c: S.Schema<never, {
                                    readonly b: {
                                        readonly a: string;
                                    };
                                }, {
                                    readonly b: {
                                        readonly a: string;
                                    };
                                }> & {
                                    readonly fields: {
                                        b: S.Schema<never, {
                                            readonly a: string;
                                        }, {
                                            readonly a: string;
                                        }> & {
                                            readonly fields: {
                                                a: S.Schema<never, string, string>;
                                            };
                                        };
                                    };
                                };
                            };
                        };
                    };
                };
            };
        };
    };
};

current .d.ts (growth is linear)

import * as S from "@effect/schema/Schema";
export declare const schema6: S.Schema<never, {
    readonly e: {
        readonly e: {
            readonly d: {
                readonly c: {
                    readonly b: {
                        readonly a: string;
                    };
                };
            };
        };
    };
}, {
    readonly e: {
        readonly e: {
            readonly d: {
                readonly c: {
                    readonly b: {
                        readonly a: string;
                    };
                };
            };
        };
    };
}>;

from effect.

gcanti avatar gcanti commented on July 19, 2024

it's important for people to embrace opaque types

I'm not sure what you mean by "opaque types" (whether you're referring to this trick: link or using S.Class?), but the amount of information that goes into the .d.ts file remains the same.

I would also be fine with exposing a combinator who extract fields instead

Mmh, I'm not sure I'm following, how can we extract the typed fields if we don't have their types stored somewhere at the type level (as you did with & { fields: Fields })?

from effect.

patroza avatar patroza commented on July 19, 2024

I'm not sure what you mean by "opaque types" (whether you're referring to this trick: link or using S.Class?), but the amount of information that goes into the .d.ts file remains the same.

yes class https://github.com/effect-ts-app/boilerplate/blob/main/_project/models/_src/User.ts#L71 (ExtendedClass), or struct retyped with interfaces inside From and To.
regarding the data that goes into the .d.ts; no it will stick to one Level. all these nested From and To, just turn into: OpaqueFrom and OpaqueTo for each field.

e.g:

type User =
& Schema<never, { readonly name: FullName.From; }, { readonly name: FullName }>
& { fields: { readonly name: typeof FullName } }

(for struct indeed it will still carry the fields nested, but also will still benefit when the members are opaque)

I would also be fine with exposing a combinator who extract fields instead

Mmh, I'm not sure I'm following, how can we extract the typed fields if we don't have their types stored somewhere at the type level (as you did with & { fields: Fields })?

yea this is what I mentioned, for each field we can get the Schema<ROfParent, From, To> out, but not Schema<ROfField, From, To>, nor PropertyDescriptor<....>
mmh but I guess renames are a problem :)

Well, the amount of information that a schema carries around definitely matters

no disagreement here.

from effect.

gcanti avatar gcanti commented on July 19, 2024

@patroza A technique I've successfully used in io-ts to avoid excessive nesting is to define an interface for each main API to save the type of the arguments:

import type * as AST from "@effect/schema/AST"
import * as S from "@effect/schema/Schema"
import type { Simplify } from "effect/Types"

type AnySchema<R = unknown> = S.Schema<any, any, R> | S.Schema<never, never, R>

interface struct<Fields extends S.StructFields>
  extends S.Schema<Simplify<S.ToStruct<Fields>>, Simplify<S.FromStruct<Fields>>, S.Schema.Context<Fields[keyof Fields]>>
{
  readonly fields: Fields
}

declare const struct: <Fields extends S.StructFields>(fields: Fields) => struct<Fields>

interface array<Item extends AnySchema>
  extends S.Schema<ReadonlyArray<S.Schema.To<Item>>, ReadonlyArray<S.Schema.From<Item>>, S.Schema.Context<Item>>
{
  readonly item: Item
}

declare const array: <Item extends AnySchema>(item: Item) => array<Item>

interface union<Members extends ReadonlyArray<AnySchema>>
  extends S.Schema<S.Schema.To<Members[number]>, S.Schema.From<Members[number]>, S.Schema.Context<Members[number]>>
{
  readonly members: Members
}

declare const union: <Members extends ReadonlyArray<AnySchema>>(...members: Members) => union<Members>

interface literal<Literals extends ReadonlyArray<AST.LiteralValue>>
  extends union<{ readonly [I in keyof Literals]: S.Schema<Literals[I]> }>
{
  readonly literals: Literals
}

declare const literal: <Literals extends ReadonlyArray<AST.LiteralValue>>(...literals: Literals) => literal<Literals>

/*
const s1: struct<{
    a: S.Schema<string, string, never>;
    b: array<S.Schema<number, string, never>>;
    c: literal<["a", "b"]>;
}>
*/
export const s1 = struct({
  a: S.string,
  b: array(S.NumberFromString),
  c: literal("a", "b")
})

// const numberFromString: S.Schema<number, string, never>
export const numberFromString = s1.fields.b.item

// const myliterals: ["a", "b"]
export const myliterals = s1.fields.c.literals

IMO it's also more readable because there are no repetitions:

declare const asSchema: <S extends AnySchema>(
  schema: S
) => S.Schema<S.Schema.To<S>, S.Schema.From<S>, S.Schema.Context<S>>

/*
const s1AsSchema: S.Schema<{
    readonly a: string;
    readonly b: readonly number[];
    readonly c: "a" | "b";
}, {
    readonly a: string;
    readonly b: readonly string[];
    readonly c: "a" | "b";
}, never>
*/
export const s1AsSchema = asSchema(s1)

i.e.

const s1: struct<{
    a: S.Schema<string, string, never>;
    b: array<S.Schema<number, string, never>>;
    c: literal<["a", "b"]>;
}>

versus

const s1AsSchema: S.Schema<{
    readonly a: string;
    readonly b: readonly number[];
    readonly c: "a" | "b";
}, {
    readonly a: string;
    readonly b: readonly string[];
    readonly c: "a" | "b";
}, never>

If desired, and solely for the purpose of readability, we could go even further and define helper interfaces to hide unnecessary type parameters (i.e., when R = never and/or I = A)

interface S<A> extends S.Schema<A> {}
interface SI<A, I> extends S.Schema<A, I> {}

type SymplifySchema<Schema extends AnySchema> = S.Schema.Context<Schema> extends never
  ? Equals<S.Schema.From<Schema>, S.Schema.To<Schema>> extends true ? S<S.Schema.To<Schema>>
  : SI<S.Schema.To<Schema>, S.Schema.From<Schema>>
  : Schema

declare const string: SymplifySchema<typeof S.string>
declare const NumberFromString: SymplifySchema<typeof S.NumberFromString>

/*
const s1Simplified: struct<{
    a: S<string>;
    b: array<SI<number, string>>;
    c: literal<["a", "b"]>;
}>
*/
export const s1Simplified = struct({
  a: string,
  b: array(NumberFromString),
  c: literal("a", "b")
})

i.e.

const s1Simplified: struct<{
    a: S<string>;
    b: array<SI<number, string>>;
    c: literal<["a", "b"]>;
}>

versus

const s1: struct<{
    a: S.Schema<string, string, never>;
    b: array<S.Schema<number, string, never>>;
    c: literal<["a", "b"]>;
}>

example with nested fields

const schema1 = struct({ a: string })
const schema2 = struct({ b: schema1 })
const schema3 = struct({ c: schema2 })
const schema4 = struct({ d: schema3 })
const schema5 = struct({ e: schema4 })

/*
const schema6: struct<{
  e: struct<{
      e: struct<{
          d: struct<{
              c: struct<{
                  b: struct<{
                      a: S<string>;
                  }>;
              }>;
          }>;
      }>;
  }>;
*/
export const schema6 = struct({ f: schema5 }) // 6 levels

// const leaf: S<string>
export const leaf = schema6.fields.f.fields.e.fields.d.fields.c.fields.b.fields.a

from effect.

gcanti avatar gcanti commented on July 19, 2024

For the sake of readability, we could define a lot of interfaces (as I did in io-ts), essentially one for each built-in schema / combinator (even for S.string or S.NumberFromString for example):

interface $string extends S.Schema<string> {}
declare const string: $string

interface NumberFromString extends S.Schema<number, string> {}
declare const NumberFromString: NumberFromString

interface option<V extends AnySchema>
  extends S.Schema<O.Option<S.Schema.To<V>>, O.Option<S.Schema.From<V>>, O.Option<S.Schema.Context<V>>>
{
  readonly value: V
}
declare const option: <V extends AnySchema>(v: V) => option<V>

/*
const schema: struct<{
    a: $string;
    b: array<NumberFromString>;
    c: literal<["a", "b"]>;
    d: option<$string>;
}>
*/
export const schema = struct({
  a: string,
  b: array(NumberFromString),
  c: literal("a", "b"),
  d: option(string)
})

// const optionValue: $string
const optionValue = schema.fields.d.value

from effect.

patroza avatar patroza commented on July 19, 2024

@gcanti yes I like it, or in @mikearnaldi speak “what a great idea!” ;)
It’s simply the wholesale continuation of Opaqueness, including the Schema. Typewise i suspect its much better performance wise but can’t say without benchmarking first.

An example with property descriptors would be nice, and eg prop mapping (different name in From) if we would implement like https://github.com/patroza/effect/pull/3/files#diff-7da43f2b7b3dea1554f2627482ba41e4f885c908848b72704d9887ffcc065153R4830.
(if im not mistaken, the benefit also is that picking these out of fields compared to current S.pick gets you the mapping transformation out too.)

I wonder if going all in or still eg exposing R would be useful somehow to make them stand out.

from effect.

gcanti avatar gcanti commented on July 19, 2024

if im not mistaken, the benefit also is that picking these out of fields compared to current S.pick gets you the mapping transformation out too

@patroza yes, this is an example using my working branch (#2172)

import * as S from "@effect/schema/Schema"

/*
const schema: S.struct<{
    a: S.Schema<string, string, never>;
    b: S.PropertySignature<"c", ":", number, ":", string, never>;
}>

i.e.

S.Schema<{
    readonly a: string;
    readonly c: number;
}, {
    readonly a: string;
    readonly b: string;
}, never>

*/
const schema = S.struct({
  a: S.string,
  b: S.propertySignatureDeclaration(S.NumberFromString).pipe(S.propertySignatureKey("c"))
})

console.log(S.decodeSync(schema)({ a: "a", b: "1" })) // => { a: 'a', c: 1 }

/*
const schema2: S.struct<{
    d: S.Schema<boolean, boolean, never>;
    a: S.Schema<string, string, never>;
    b: S.PropertySignature<"c", ":", number, ":", string, never>;
}>

i.e.

S.Schema<{
    readonly a: string;
    readonly c: number;
    readonly d: boolean;
}, {
    readonly a: string;
    readonly b: string;
    readonly d: boolean;
}, never>

*/
const schema2 = S.struct({
  ...schema.fields,
  d: S.boolean
})

console.log(S.decodeSync(schema2)({ a: "a", b: "1", d: true })) // => { d: true, a: 'a', c: 1 }

from effect.

gcanti avatar gcanti commented on July 19, 2024

For the sake of readability...

Update from the working branch

import * as S from "@effect/schema/Schema"

/*
const current: S.struct<{
    a: S.literal<["A"]>;
    b: S.literal<["B", "C"]>;
    c: S.NumberFromString;
    d: S.PropertySignature<never, "?:", number | undefined, "?:", number | undefined, never>;
    e: S.tuple<[S.$string, S.$number]>;
    f: S.array<S.$string>;
    g: S.union<[S.$string, S.$number]>;
    h: S.nonEmptyArray<S.$string>;
    i: S.struct<{
        a: S.$string;
    }>;
}>
*/
export const current = S.struct({
  a: S.literal("A"),
  b: S.literal("B", "C"),
  c: S.NumberFromString,
  d: S.optional(S.number),
  e: S.tuple(S.string, S.number),
  f: S.array(S.string),
  g: S.union(S.string, S.number),
  h: S.nonEmptyArray(S.string),
  i: S.struct({
    a: S.string
  })
})

/*
const old: S.Schema<{
    readonly a: "A";
    readonly b: "B" | "C";
    readonly c: number;
    readonly e: readonly [string, number];
    readonly f: readonly string[];
    readonly g: string | number;
    readonly h: readonly [string, ...string[]];
    readonly i: {
        readonly a: string;
    };
    readonly d?: number | undefined;
}, {
    readonly a: "A";
    readonly b: "B" | "C";
    readonly c: string;
    readonly e: readonly [string, number];
    readonly f: readonly string[];
    readonly g: string | number;
    readonly h: readonly [string, ...string[]];
    readonly i: {
        readonly a: string;
    };
    readonly d?: number | undefined;
}, never>
*/
export const old = S.asSchema(current)

from effect.

patroza avatar patroza commented on July 19, 2024

@gcanti wow, that looks great!

b: S.propertySignatureDeclaration(S.NumberFromString).pipe(S.propertySignatureKey("c"))

one thing I would ponder on is, I personally like to think of my To schema as the center of the universe, not my From.
I mean this seems a little counter intuitive when one thinks logically about From->To, but still. In my current experimentation, I use a MapFrom, not MapTo.
so I would define C, maps from B, not B maps to C.
it's in the same vein as
"Number" From "String" though.
"C" from "B"

You don't say
"Number" To "String", so neither
"B" to "C" imo

from effect.

gcanti avatar gcanti commented on July 19, 2024

The name aligns with the type: NumberFromString: Schema<number, string> (number from string), but for the transformations, for now, I've done it this way because it seems to me that people prefer to think of transformations as From -> To, all the other transformation APIs are designed this way (e.g. transform(from, to, ...), transformOrFail(from, to, ...), transformLiteral(from, to))

from effect.

patroza avatar patroza commented on July 19, 2024

The name aligns with the type: NumberFromString: Schema<number, string> (number from string), but for the transformations, for now, I've done it this way because it seems to me that people prefer to think of transformations as From -> To, all the other transformation APIs are designed this way (e.g. transform(from, to, ...), transformOrFail(from, to, ...), transformLiteral(from, to))

I think for the apis it might make sense, but I strongly believe when you define a struct or a class, the properties define the To (aka the type/schema you're actually defining).

I find it very strange to say person.a, while person has a b field somehow mapped to a

from effect.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.