First-class types

Hello, I would like to suggest making types first-class objects. Reason: There are many operations where compile-time type checking is not enough: parsing JSON, validating an object against a specific schema, protocols verification, constructing a database schema type-based, etc. Having first class types or being able to write functions using types as arguments would help with these tasks.

1 Like

That’s what I’m trying to solve with rescript-struct

1 Like

Its realy works solution, but its looking bit ugly and unreadable. One of most powerful reason, why i moved to functional languages (and may be other programmers too) is brief code and domain declarations, compared with OOP languages. But things like superstruct or runtypes, which realy solve preblem of missing first-class types in TS and rescript sacrifice all this benefits.

I would like rescript to avoid the same mistake that typescript made and add the ability to make types first-class objects. or add a number of functions for obtaining schema types that will work out at compile time

I think you will need to describe what you are after here. Have you tried to solve your problem using the module language (eg with first-class modules)?

Edit: What I mean is that Rescript (and OCaml) don’t have first class types, but they do have first class modules, and the module language does let you abstract over parameterized type constructors. So what I mean is, is your task able to be solved with the module language (ie what have you tried).

I haven’t explored the module system yet, but it seems to me that defining domain entities and data as modules is a bit of a controversial design decision. It seems to me that it is necessary to add the ability to at least get the type scheme and produce something from it …

It’s actually pretty idiomatic to use modules for domain modelling in OCaml/Reason/ReScript, so if you haven’t explored them yet, you don’t know what you’re missing.

3 Likes

I agree with hoichi…modelling your domain with types and modules in this type of language is quite normal. Could you explain what you mean about it being controversial? (Sorry, I know this is getting a bit far from your original question, but I’m curious what you mean.)

I would imagine some example code like this:

type money = {
    val: int,
    currency: string,
}

let payment = parseJsonStrictly<money>(someOuterStringInput: /* : string */) 
// => Result<money, Error>

let tableScheme = createTableScheme(money) // => Object some scheme
// which able to use for createing migrations for example

let verificatedProtocolMsg = {
    data: someData //: money,
    scheme: getJsonScheme(money)
}
//Last case is useful for verify messaging between separated services
//which oftenly subject to change and this reason have a risk
//uncatched one-side protocol deformation

with modules we can create in module type and specific functions to work with it. With first-class types we can declare one-time functions produces some from type-schema and use them everywhere we want. And it will very briefly.

If I understand you correctly, I think you would do well to look in to first-class modules.

You want to pass in the type, but you need to pass in the module you need instead. In Rescript (and OCaml…) it needs to be like that.


Here’s just a little example to show you about first-class modules. (There are more examples of using them in this forum, and also in OCaml, eg here.)

You could imagine a module type like this, filled with the function signatures you need, but abstract in the type.

module JsonParseable = { 
  module type S = { 
    type t
    let parseJson: string => result<t, ...>
    ...
  }
 }

Then the parse json you want takes the first class module that fulfills that signature and a value of that type. Imagine something like:

let parseJson = (type a, module(M: JsonParseable.S with type t = a), v) => M.parseJson(v) 

Then you need to implement what you need of course

module Money = {
  type t = ...
  let parseJson = ...
  ...
}

and use it

let money = parseJson(module(Money), ...)

but that parseJson function that takes the first class module, is working like that one you showed…it just needs the module passed in explicitly.

(But also, take a look at DZakh’s rescript-struct) if you need the json parsing.)

I’ve reviewed the documentation for modules, and didn’t see how a functor can take a type as a parameter.

I might as well write a parseMoney() or jsonMoney() or schemeMoney() function. It doesn’t solve the problem. First class types would allow one abstract function to be written one time and can be applied to any type. In your case, if I understand correctly, you will have to make a separate implementation for each type.

The problem is not to place the function in the same brackets with the type, but not to repeat similar logic.

First-class or reified types can be done at the library level. It doesn’t necessarily need compiler level support–maybe some codegen or PPX to make it easy to write schemas. In fact we should prefer it to be at the library level, because that means the types would be encoded as normal values which can be composed and manipulated just like any other values, which is a big part of the appeal of FP.

E.g. here’s an implementation done in Scala: Introduction to ZIO Schema | ZIO

Yawaramin, are you talking about something like this: Types as first class citizens in OCaml - #3 by ivg - Learning - OCaml ?

Edit: I know nothing about scala…is there an example in ocaml (or even at least haskell)?

mnemesong, I think I am misunderstanding your true problem. The first class modules look like it solve the code you gave, but if not, then I have misunderstood you, apologies.

Are you wanting a type of dynamic typing encoding maybe (as in the link I posted in the last post)?

The code you provided really solves the code I gave, provided that this is the only case. If you need to carry out similar manipulations with other types, you will have to write a similar construction for them all over again. What can be achieved is the similarity of the interfaces of the modules, but not the implementations.

Dynamic typing is a near-crutch way to solve the problem, because we take a language with a beautiful and sound type system and saw a metalanguage on functions to write types on them …

In all practically used languages, it is possible to access types. In Scala and f#, these are classes that can be reflected on. In ocaml, the first letter indicates that there are objects there
(although I don’t know how reflection or declaring types through objects is possible there). In Clojure, you can access it either through viewing the fields of a class, or through macros or reflection.

Perhaps it’s worth just adding a decorator like @schema('t) for obtaining a type schema to the rescript: it is output for compilation anyway and printing it to the source code in any form does not look like a difficult task.

Hi Ryan, I think the page I linked to explains it pretty well. Just ignore the fact that it’s talking about Scala, and mentally substitute the following concepts:

  • case class: record type
  • sealed trait: variant type

Imagine having a schema for any type and being able to automatically derive codecs for different encodings, like JSON, Protobuf, Thrift, MessagePack, etc. It’s all about taking a ReScript type and marshalling/unmarshalling its values across many different formats.

EDIT: just saw your question about ivg’s message in OCaml Discuss. So in ReScript terms it would look something like (note, I’m making up a Schema module here but in practice it would look similar):

module Person = {
  type t = {
    name: string,
    age: int,
    id: string,
  }

  let schema = { // let schema: Schema.t<Person.t>
    open Schema

    let encode = {name, age, id} =>
      array([
        string(~name="name", name),
        int(~name="age", age),
        string(~name="id", id)
      ])
    let decode = encoding =>
      switch encoding {
      | Array([String("name", name), Int("age", age), String("id", id)]) =>
        {name, age, id}
      | _ =>
        errorDecoding(encoding)
      }

    make(encode, decode)
  }
}

let personA = {Person.name: "A", age: 30, id: "1"}

let protobuf = Protobuf.encode(Person.schema, personA)
let json = Json.encode(Person.schema, personA)

And so on.

2 Likes

yawaramin: Thanks for the explanation and example code, I appreciate it.

mnemesong: Okay, I understand you know I think, thanks for your clarification. I thought you were talking more about how to get functionality similar to type classes (a la haskell), or something like that. You’re talking about runtime type reflection I think.

1 Like

Maybe something like design:type of reflect-metadata (https://www.npmjs.com/package/reflect-metadata) would be practical?
Example: How to get type data in TypeScript decorator? - Stack Overflow

Since decorators are a stage 3 proposal (GitHub - tc39/proposal-decorators: Decorators for ES6 classes) it could be a feature in rescript anyways?!

Yes, this solution looks good!

Additionally, it might be nice to create a compile-time function a la @scheme('t) => SchemeObj as in the example, because that’s the most general case - but that’s syntactic sugar. Decorator-style solutions would also suffice for the problem we’re discussing.

It looks like what you need https://twitter.com/dzakh_dev/status/1646138021794725890

Later the struct might be used for parsing, serializing, generating json schema, OpenApi (WIP), building forms, and more.

3 Likes