Skip to content

Should we have an unknown top-level type #6119

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
jmagaram opened this issue Apr 9, 2023 · 16 comments
Closed

Should we have an unknown top-level type #6119

jmagaram opened this issue Apr 9, 2023 · 16 comments

Comments

@jmagaram
Copy link
Contributor

jmagaram commented Apr 9, 2023

I saw your note saying there is an unknown type somewhere in JSError but I couldn't find it or determine if it resolves the issue here. I've been experimenting with some code that makes use of the unknown type. Below is an example. Notice the syntax error at the very bottom. Is it possible to create an unknown type that you can safely assign anything to without requiring the developer to do something like Unknown.make? In TypeScript there are two top-types any and unknown (see here). In ReScript whenever I see 'a that looks like any to me. I've seen ReScript type definitions that use something like <+'a> and thought maybe that technique would help but I don't understand it.

In the example below, I want developers to author instances of Pattern. It is safest to author these when isTypeOf takes an Unknown.t because if it were just an 'a then the developer, if not careful, could treat the function parameter like a string or anything when it really isn't, and this can cause run-time crashes. But when using a Pattern I want to be able to pass in anything and not have to manually covert it to unknown using Unknown.make which is kind of weird. So from inside the isTypeOf the parameter should look like Unknown.t but when calling it from the outside I want it to accept an 'an any.

If this isn't possible, I wonder if we should have a unknown top-level type exposed in the language. This would be useful to provide more safe external functions; make them return an unknown if we don't know exactly what we're going to get. We could use this in Object module of Core when we're getting property values. And maybe genType exports these as unknown without any shims.

module Unknown = {
  type t
  external make: 'a => t = "%identity"
}

module type Pattern = {
  type t
  let isTypeOf: Unknown.t => bool
}

module NonEmptyString: Pattern = {
  type t = string
  let isTypeOf = (u: Unknown.t) =>
    switch u->Js.Types.classify {
    | Js.Types.JSString(s) => s->Js.String2.trim->Js.String2.length > 0
    | _ => false
    }

  // Unsafe and unpredictable if not a string
  // let isTypeOfUnsafe = u => u->Js.String2.trim->Js.String2.length > 0
}

let x = 45->Unknown.make->NonEmptyString.isTypeOf // Works but weird
let y = 45->NonEmptyString.isTypeOf // Syntax error!
@cristianoc
Copy link
Collaborator

The unknown type is in the compiler, it's called unknown and is a predefined type.
The make function is correct, as any value has unknown type. There's no such function in the language, but could be added to Core.
Perhaps this should be moved to Core.

@jmagaram
Copy link
Contributor Author

jmagaram commented Apr 10, 2023

I'm advocating to expose the unknown type in the language itself maybe as a keyword like string or int. Definitely not specific to the Core library. And also that the user never needs to call some "make" function. Instead the compiler allows assignment to unknown always because it is always safe. Just like TypeScript. It is awkward to use a make function for that.

@jmagaram
Copy link
Contributor Author

jmagaram commented Apr 10, 2023

In case I wasn't clear... in my own code I made my own Unknown.t that is just an abstract type that does nothing. Using it is awkward and weird because if you want to call a function that has a parameter of type unknown you have to call Unknown.make which doesn't really do anything and always succeeds. The feature I'm missing is the ability to assign any value to an unknown without calling a make function. That is what requires compiler support.

A bunch of functions take a 'a which is anything. It would be weird if you had to first call Anything.make to use those functions. Same with unknown.

@cristianoc
Copy link
Collaborator

It's clear. This is for Core.

@jmagaram
Copy link
Contributor Author

Ok I wish I understood what you're saying. I'm curious how you envision this in Core. Will there be a Types.unknown type or something like that? Will I be able to assign values to unknown without explicitly calling a make function? Does this mean Core is required to use unknown?

@cristianoc
Copy link
Collaborator

cristianoc commented Apr 10, 2023

Sorry if I was unclear. Type unknown exists already.
What would go in Core is:

type t = unknown
let make = ...

in Unknown.res. Just a little convenience.

@jmagaram
Copy link
Contributor Author

So if I have a function that takes an Unknown.t and I want to call it I will ALWAYS have to do something like this below.

let f = (u:Unknown.t) => ...
let something = "abc"
f(something->Unknown.make)

Or can you make the compiler do something special with Unknown, like TypeScript, so anything can be assigned to it without calling the make function? I understand you are probably overwhelmed doing almost all the work on the compiler - the most important part of ReScript - and so I can see why this might not be top priority. But I just want to make sure you understand what I'm asking for here. Unknown.t is special because it might be the only type, other than anything, you can safely assign anything to. Without that automatic safe assignment it becomes cumbersome/awkward to use.

@cristianoc
Copy link
Collaborator

So if I have a function that takes an Unknown.t and I want to call it I will ALWAYS have to do something like this below.

let f = (u:Unknown.t) => ...
let something = "abc"
f(something->Unknown.make)

Or can you make the compiler do something special with Unknown, like TypeScript, so anything can be assigned to it without calling the make function? I understand you are probably overwhelmed doing almost all the work on the compiler - the most important part of ReScript - and so I can see why this might not be top priority. But I just want to make sure you understand what I'm asking for here. Unknown.t is special because it might be the only type, other than anything, you can safely assign anything to. Without that automatic safe assignment it becomes cumbersome/awkward to use.

Thar's correct. One can add a subtyping rule (so one does not need to call make), but really it seems overkill for this use.

@cristianoc
Copy link
Collaborator

cristianoc commented Apr 10, 2023

If people start using unknown very extensively, then the subtyping rule can be considered.

@cristianoc
Copy link
Collaborator

cristianoc commented Apr 10, 2023

Notice a more compelling, but analogous, request would be to automatically promote (certain) values to option so one does not need to use Some.

@cristianoc
Copy link
Collaborator

(some) people would likely object that it is "too magic" -- basically optimising the expert's use case at the expense of possibly confusing beginners

@cristianoc
Copy link
Collaborator

It's up to debate, but likely the ecosystem might benefit more from a 10X influx of beginners than 2X influx of experts.
So it's not clear how to balance these things.

@jmagaram
Copy link
Contributor Author

The beginner will go for Unknown.make and the more experienced person discovers they can skip it. Maybe a compiler warning/info tells people that function is deprecated/unnecessary so everyone figures it out.

Very interesting case of Some - hadn't thought of that.

If someone writes a function that should take an unknown they can write it to take an any which makes using that function very convenient. They just have to be careful when writing that function not to treat the parameter as if it is something it is not and generate run-time exceptions. When using external functions that return unpredictable values it is safer to mark them as returning an unknown. So using unknown helps beginners do the right thing while it won't matter much for experts.

I was imagining a future version of unions would have people author modules with the shape like this. Each option in the union would be a module name. So discrimination and pattern matching happens with code the user writes which is totally customizable based on typeof, instanceof, or a JSON parsing library. Any value of type PositiveInt.t, Success.t, Failure.t, etc. could be safely assigned to the union. And the equals enables optimized equality testing. Literals could have special syntax. Each built-in module like Int, String etc. could have these functions (just like Array.isArray) so you could make a union of string and ints and bigints without any special work in the compiler. In this scenario, developers would write functions that take an unknown.

module Kind = {
   type t
   let isTypeOf : unknown => bool
   let equals : (t,t)=>bool

type t = PositiveInt | String | Success | Failure

I agree we have no idea who would use unknown types and need to see how things play out to determine the importance of it.

@jmagaram
Copy link
Contributor Author

HUGE misunderstanding here. Maybe I never actually tried typing unknown into my Rescript and so I created my own thing Unknown.t which was totally unnecessary. I created a PR on Core that Glenn looked at and he didn't tell me unknown already existed. I raised an issue about it on Core and no one commented until much later except for you, and I thought you meant it was a type buried away somewhere inside the compiler but not actually exposed. It isn't in any of the online docs. I've had other interactions with people and no one said "hey that already exists". I was shocked that tonight I could just type unknown anywhere and it works. I read your comments again above and you make it clear this exists as a predefined type. You must think I'm crazy or "what the hell is this guy saying we need this thing for when it already exists!"

Should this be added to the docs as a built-in type?
https://rescript-lang.org/docs/manual/latest/primitive-types

@cristianoc
Copy link
Collaborator

Glad it's clear now.
Yes a PR to the docs would be great.

@jmagaram
Copy link
Contributor Author

Please see rescript-lang/rescript-lang.org#670

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants