RFC: Class hierarchies in WebAPI, and developing WebAPI, React and the compiler together
Maintainer thường phản hồi trong vòng 1 ngày
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 5/5
- Thời gian dự kiến
- Hơn một tuần
- Mức phù hợp với người mới
- 25/100
- Loại issue
- Tính năng
- Độ rõ ràng
- Đặc tả rõ ràng
- Mức độ hoạt động
- Sôi nổi
- Công nghệ
- javascript, ocaml
- Lĩnh vực
- backend-api-design
Hướng nghiên cứu
Start by reading the RFC’s sections on the compiler proposal, especially A–C, then inspect the compiler’s type attributes and untagged variant handling; the issue does not name specific source files or tests. The work spans compiler semantics, WebAPI bindings and React bindings, and includes an unresolved decision about mutable fields on private records. Done means the team has settled the proposals and implemented and validated the compiler and binding changes across the stated development sequence.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
Summary
ReScript v13 lets a type say which JavaScript class its values are instances of, and an untagged variant then tells its cases apart with instanceof, subclasses first. This already replaces hand-written classifiers: matching re-classifies a value by its runtime class.
WebAPI models interfaces as records, and records can't carry a class yet. This RFC proposes two compiler features for that, A and B, plus one prerequisite fix, and leaves a third question, C, open. It also proposes how to develop WebAPI, the React bindings and the compiler together while these features settle, and asks the WebAPI team for feedback on both.
What the compiler does today (v13)
A type declares its runtime class with @instanceOf, or its typeof with @typeOf:
@instanceOf("Animal") type animal
@instanceOf("Dog") type dog = private animal // Dog is a subclass of Animal
- A subclass is a private abbreviation of its superclass:
(dog :> animal)upcasts, but ananimalis never taken for adog. - An untagged variant tests its cases in a safe order: literals first, then
typeofprimitives, then classes, subclasses before their superclasses, then functions and objects:
@unboxed type pet = Dog(dog) | Animal(animal) | Name(string)
if (typeof pet === "string") { ... }
else if (pet instanceof Dog) { ... }
else { ... } // Animal: the last case of a complete match is not checked
- A class introduced on an abbreviation must be
private, so two names for one type can't claim two different classes. That is how the stdlib's typed arrays became distinct types in v13 (#9050):Int8Array.tisprivate TypedArray.t<int>. - Classification needs no helper. A constructor doesn't fix which case a value matches, so a value wrapped in its superclass case is re-classified by matching:
let isDog = (a: animal) =>
switch Animal(a) { // `Animal(a)` compiles to `a`
| Dog(_) => true // `a instanceof Dog`
| Animal(_) => false
}
Bindings can also type fields and results as the union directly, so that users just match on them.
Why this matters for WebAPI
With class annotations, WebAPI could:
- drop the hand-written classifiers (
isInstanceOfvia%raw,Element.classify,FillStyle.decode). Fields and results that can hold several classes get a union type, and users match on it; - write sound unions such as
blobPart = String | Blob | ArrayBuffer | TypedArray. TheArrayBufferand typed-array cases are commented out today; they work now that typed arrays are distinct types; - upcast with
:>instead of%identity, once the compiler knows which record extends which (B below).
What doesn't work yet
WebAPI models an interface as a record of its fields, inherited fields included, with methods coming from Impl functors:
type blob = {size: float, @as("type") type_: string}
type file = {...blob, name: string, lastModified: float}
Three things stand between that and class annotations:
@instanceOfcan't go on a record definition.@instanceOf("Blob") type blob = private {...}is rejected ("only abstract types and type abbreviations"). The workaround of a fields record plus@instanceOf("Blob") type blob = private blobFieldsdoesn't help either: fields can't be read through a private abbreviation, sob.sizestops compiling.- Record inheritance isn't visible to the compiler.
filecontainsblob's fields but isn't an abbreviation ofblob, so nothing says File is a Blob. InBlob(blob) | File(file)theinstanceof Blobcheck would come first and take every File. For the same reason(file :> blob)between private records is refused: structural coercion into a private record would let any record withblob's fields pass as a Blob (see Prerequisites). - Records carrying a class must be private, or
{size: 1.}would type as abloband be dispatched as one. WebAPI records have mutable fields (element.id,innerHTML, …). Writing a mutable field of a private record is warning 111 since ReScript 13.0.0-alpha.5 (#8366), so every DOM property write would warn. This is probably why the WebAPI sources still say "TODO: mark as private once mutating fields of private records is allowed".
Considered: abstract types with accessors
Interfaces could instead be abstract class types, @instanceOf("Blob") type blob and @instanceOf("File") type file = private blob, with properties read and written through generated @get/@set externals. That needs no compiler change: ancestry, recognition and :> each already have one mechanism. The cost is record syntax, b->Blob.size instead of b.size, and the generator emitting the inherited accessors for every type.
WebAPI chose records deliberately, for the developer experience, so this RFC continues with records. Abstract types remain a fallback that works today.
Prerequisites for A
- Coercion into a private record must be refused (#9124, fixed by #9125). Until then a plain record coerces into a private one with the same fields:
Iftype plain = {size: float} type blob = private {size: float} let fake = (p: plain) => (p :> blob) // compilesblobcarried@instanceOf("Blob"), any literal could pass as a Blob. #9125 refuses this for every private record, sinceprivatemeans it can't be constructed outside its module, and:>from a plain record is a way to construct it. If that breaks real code, it can be narrowed to records with@instanceOf. - Mutability in record coercion: fixed by #9086 (#9063).
{x: float}no longer coerces to{mutable x: float}.
Proposal: compiler side
A. @instanceOf on private record definitions. The record type becomes a class case, dispatched with instanceof, and its fields stay readable. It must be private, by the same rule as above. A parameterized private record works like a parameterized abstract type: the dispatch ignores the parameters, so a variant case can use a concrete instantiation (Element(element<unknownNativeElement>)).
B. An explicit superclass: @extends.
@instanceOf("Blob")
type blob = private {
size: float,
@as("type") type_: string,
}
@instanceOf("File") @extends(: blob)
type file = private {
...blob,
name: string,
lastModified: float,
}
let describe = (f: file) => `${f.name}: ${Float.toString(f.size)} bytes` // fields readable
let asBlob = (f: file) => (f :> blob) // plain upcast
@unboxed
type blobPart =
| Text(string)
| Blob(blob)
| File(file)
| Buffer(ArrayBuffer.t)
| Bytes(Uint8Array.t)
if (typeof part === "string") { ... }
else if (part instanceof File) { ... } // before Blob, because of @extends
else if (part instanceof Blob) { ... }
else if (part instanceof ArrayBuffer) { ... }
else { ... }
@extends(: blob)names a type, so it works across modules (@extends(: Blob.t)) and inside recursive type groups. That matters for the DOM, which is largely one recursive group.- Inherited fields can be spread from a parameterized fields record defined outside the group, as the React Native bindings already do:
type nodeFields<'node, 'element> = {parentNode: null<'node>, parentElement: null<'element>} @instanceOf("Node") type rec baseNode = private {...nodeFields<node, element>} @instanceOf("Element") @extends(: baseNode) and element = private {...nodeFields<node, element>, tagName: string} @unboxed and node = Element(element) | Other(baseNode)@extendschecks the fields after spreads are expanded, so this pattern works and gets a test case. A union in the same recursive group as its classes dispatches subclass-first, as it does for abstract class types today. - The compiler would check that the named type is a class, and that the record has all its fields with the same types, so that
:>is valid. It would report a missing@instanceOf, a non-class supertype, missing fields and cycles as errors. - Field composition and class inheritance are different facts: a spread of mixin fields implies no ancestry. So the explicit form comes first. Inferring the superclass from a spread could be an optional shorthand later.
C. Writable mutable fields on private records. This is a decision about private records in general, not about classes, so it is open and best decided on its own:
- C1. No warning 111 for fields declared
mutablein a private record:privatethen means "can't be constructed outside its module", not "can't be written". This is the simpler option, with no new attribute. - C2. An opt-in attribute on the type allows writes, keeping today's warning by default. This keeps write permission separate from runtime recognition, at the cost of an opt-in.
- C3. Keep the warning, and leave WebAPI's records public, without classes.
Proposal: WebAPI side (once A, B and C exist)
- The generator emits
@instanceOf("X")and@extends(: parent)for every IDL interface with a global constructor. Mixins (ParentNode,GlobalEventHandlers) and dictionaries get no class, since dictionaries are plain objects. - Interface records become
private. %identityupcasts (asNode,asBlob, …) become:>, which B makes valid.- Fields and results that can hold several classes are typed as unions, which replaces
isInstanceOf,classifyanddecode. - Unions like
blobPartget theirArrayBufferand typed-array cases back.
Proposal: developing WebAPI, React and the compiler together
- Core React stays platform-agnostic, because rescript-react-native builds on it. Only a React DOM part depends on WebAPI, instead of on the stdlib's
Dommodule. React DOM useselement,eventTarget,event,window,history,location,touchList,dataTransferandeventPointerIdfrom there. How to split the bindings is up to rescript-react. - The stdlib's web types go away.
Dom, and File, Blob and similar, are leftovers in the stdlib. The proposal is to deprecate them in 12.x and remove them in v13, with WebAPI as the replacement. - The compiler repo vendors a WebAPI base package, as it vendors the React bindings today (
tests/dependencies/rescript-react). The tests, examples and playground need React DOM, which then needs the base package. The vendored subset is what React DOM needs: EventTarget, Node, Element and HTMLElement, the event types, Window, History, Location, DataTransfer, TouchList and the style object. - While the features above are in flux, one compiler PR changes the compiler, the vendored WebAPI and the vendored React bindings together. That way every compiler feature is tried on real bindings before it's final. During that time the vendored copy is authoritative for the base subset. A
VENDORED.mdrecords the upstream commit and lists local deviations, and the changes are ported back into the WebAPI generator in batches. - Long term, the base package comes from the WebAPI repo, as the React bindings come from rescript-react, and the compiler repo only vendors a copy. Once A, B and C are released, upstream is authoritative again and the copy is re-synced from the generator.
DOM values on the server
Server runtimes (Node, Deno, Bun) have no DOM: there is no global Element, Node or Window. On the server, a class check like x instanceof Element therefore throws ReferenceError: Element is not defined. The compiler will not guard these checks, because DOM values should not show up in server code at all, and the error names the missing class. Primitives and literals never reach a class check.
React Native 0.85 installs Node, Element, HTMLElement, CharacterData, Text and Document as globals at app startup, so class dispatch works there.
Possible follow-up
Coercing a case's payload type into an @unboxed union, (e :> node), as sugar for the constructor Element(e). It would follow the existing rule that lets string coerce into an @unboxed variant with a string case. It isn't needed for this RFC.
Proposed order
- Refuse coercion into private records (#9125).
- Vendor the WebAPI base subset unchanged, and switch the vendored React DOM bindings from the stdlib's
Domto it. - Deprecate the stdlib's web types in 12.x; remove them in v13.
- A and B in one compiler PR, updating the vendored WebAPI and React bindings.
- C in its own compiler PR, once decided.
- Port the changes to the WebAPI generator and re-sync.
Questions for the WebAPI team
- C: which of C1, C2 and C3 would you choose?
- Which modules belong in the base package that React DOM depends on, and should it be published as its own npm package from the WebAPI repo?
- Are you fine with the vendored copy leading during the transition, with batched ports back to the generator?
- Ngôn ngữ chính
- OCaml
- Star
- 7.5k
- Fork
- 483
- Merge trung bình
- 20 giờ 19 phút
- Pull request đã merge (30 ngày)
- 242
Chuẩn bị môi trường
Khởi chạy dev container của dự án ngay trên trình duyệt, bằng tài khoản GitHub của bạn.
- Không có Dockerfile hay tệp Docker Compose
- Có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của rescript-lang/rescript
-
Record coercion can construct a private record from outside its moduleCó thể đã có người làm @cknitt đã nhận hôm nay. Đang mở
Độ khó 4/5 1-2 ngày Mức phù hợp với người mới 52/100
rescript-lang/rescript#9124 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 56/100
rescript-lang/rescript#9123 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 4/5 1-2 ngày Mức phù hợp với người mới 55/100
rescript-lang/rescript#9122 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 15/100
rescript-lang/rescript#9109 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Formatter: five cases where comments and doc comments are placed wronglyCó thể đã có người làm @cknitt đã nhận hôm nay. Đang mở
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 35/100
rescript-lang/rescript#9107 ·
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của rescript-lang/rescript
Issue tương tự
-
priority:low type:bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 84/100
crazy-goat/rabbit-stream#953 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 85/100
FalkorDB/FalkorDB#3597 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 2 ngày
-
C-bug O-community X-blathers-untriaged
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 84/100
cockroachdb/cockroach#176210 · 1 bình luận ·
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 67/100
-
fix: reject sourceStorage/packagedStorage bucket that differs from the stack default (fail loud)Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 82/100
Eyevinn/open-videocore#1300 ·
Maintainer thường phản hồi trong vòng 1 ngày