-
Notifications
You must be signed in to change notification settings - Fork 205
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
Sum/union types and type matching #83
Comments
First note that it should be possible to categorize this issue as a request or a feature. The request would articulate a non-trivial and practically relevant shortcoming in terms of software engineering that would justify considerations about adding new features to Dart. A feature description is a response to a request, that is, every feature under consideration should be grounded in a need for the feature in terms of an underlying software engineering problem, it should never just be "I want feature X just because that's a cool concept to have in a language". It may well be a non-trivial exercise to come up with such a request, and it may not necessarily have just one feature as its "corresponding solution", but that's a good thing, not a bug. ;-) This issue is clearly a feature issue, specifically promoting a language construct (or a family of closely related ones). So we need a request issue which describes a software engineering problem more broadly, such that some notion of sum types would be an appropriate response to that request. That said, requests for sum types are of course not a new thing. A couple of pointers: https://github.com/dart-lang/sdk/issues/4938 has been the main location for discussing union types, and the feature discussed in this issue may or may not be covered under that umbrella. In particular, this proposal may or may not assume tags. If tags are assumed, class Json {}
class JsonMap implements Json {
Map<String, Json> value;
}
class JsonList implements Json {
List<Json> value;
}
class JsonLiteral implements Json {
... // Whatever is needed to be such a literal.
} This would provide run-time support for discriminating all cases, because anything of type "Map<String, JSON>" would actually be wrapped in an object of type However, if we assume untagged unions then we cannot expect run-time support for O(1) discrimination. For instance, a recursive untagged union type could be defined like this (assuming that Dart is extended to support recursive union types): typedef NestedIntList = int | List<NestedIntList>; If we're simply considering the type So we need to clarify whether sum types are intended to be similar to SML algebraic data types (that is, tagged unions), or similar to TypeScript union types (untagged unions). We should note that TypeScript does not support dynamic type tests (and I do not believe the user-defined workaround is a relevant replacement in Dart). Dart will remain sound in the sense that the obvious heap invariant ("a variable of type |
Sorry for the missing tag, it seems I'm unable to edit them, I'll propose them in the title while I'm looking for documentation on how to do it. Thank you for the extensive explanation, I didn't dive deep enough into the sdk issues and I apologize. To answer the specification question: I was not aware of union types in TypeScript as I've not used it yet so I intended them to be as similar as possible to SML data types however I was ignoring the implementation details of such feature. |
@GregorySech, first: Thanks for your input! I should have said that. Second: I guess there's some permission management in place which restricts the labeling; I'm sure those labels will be adjusted as needed, so don't worry. So the main point is simply that it would be really useful in the discussion about a topic like this to have a characterization of the underlying software engineering problem (without any reference to any particular language construct that might solve the problem), and it would be helpful if you could capture that underlying software engineering challenge for which such a feature as 'sum types' would be a relevant solution. |
Would this still be true if Dart adds unboxed types in the future? For example, imagine in the future non-nullable |
|
If Dart is extended to support a notion of unboxed types I would expect such instances to be compatible enough with the rest of Dart to allow a reference of type You may or may not want to say that a boxed entity of type With a wrapper which is used as the representation of a tagged union type there is a transition from one type to another type when the wrapper is added. An example in SML would be the following: datatype IntString = INT of int | STRING of string;
fun f (INT i) = "An integer"
| f (STRING s) = s;
val v1 = f (INT 42);
val v2 = f (STRING "24"); Here, An extension of Dart with untagged unions would be different in many ways. I just wanted to point out the fact that this distinction matters, so we shouldn't discuss union types (or sum types) without knowing which kind we're talking about. ;-) |
Right. We knew from day one that |
@eernstg Thanks for the explanation! I misread your "wrapper object" as boxing. Yes, something like tagged unions does not require boxing. For example, Rust's and Swift's |
@yjbanov wrote:
Right, but I think the situation in Rust and Swift is slightly different from the situation in Dart. In general, a range of memory cells does not have to be a full-fledged heap entity in order to be able to contain a primitive value (like an integer or a pointer to an immutable string (Rust: But at this point we do not have general support for non-heap entities in Dart of any other size than that of a primitive, so if we want to support that kind of composite entity in Dart I think we will have to keep them in the heap (that is, we need to box them). That could still be useful, however, because an enum with an associated value is similar to a tagged union where the enum value itself is the tag. The ability to work on unboxed entities of arbitrary size would be an orthogonal feature in Dart, and we would be able to introduce support for this separately (and sort out how they would work in dynamic code), and then we could use it for entities of any kind, including enum-with-associated-value. |
If this is implemented + non-nullable types, then Dart will be the best language ever! |
Let's say that Dart had a non-nullable type ancestor called Thing, Object might be Thing | Null. I'm still unaware of implementation implications for this features to work together but the message I'm trying to convey is that this might save a lot of refactoring if non-null is implemented. |
Hey Erik, since you asked for "software engineering arguments" further up, I will bite and state why algebraic data types and pattern matching is useful from a software engineering perspective. The simple truth is: Today, NULL/optional, enums etc can all provide only half-hearted solutions to case distinction. The paper "Matching Objects with Patterns" (Emir, Odersky, Williams) and also my thesis "Object-oriented pattern matching" discusses the benefits and drawbacks of the object-oriented and other encodings of algebraic data types. The "type-case" feature is also discussed, but not a notion of untagged sum type. I'd be an advocate for the tried and tested algebraic data types which is a way to represent tuples, sum types, "value classes" and a whole bunch of other programming situations that all help with modeling and dealing with structured data. Algebraic data types would reflect the symmetry between product types and sum (=co-product) types, as known from category theory and its manifestations in functional programming in Haskell, Scala, ocaml aka Reason etc. What better software engineering argument than "it works" can there be? : ) Let me know if this is the appropriate issue or I should file a new one. (updated, typo) |
(Hello Burak, long time no c, hope everything is well!)
Agreed, I frequently mention that argument as well. And I'd prefer to widen the gap, in the sense that I would like to have one kind of entity which is optimized from access-from-outside ("data") and another one which is optimized for encapsulation and abstraction ("objects").
I think they would fit rather nicely into Dart if we build them on the ideas around enumerated type with "associated values", similarly to Swift enumerations (but we'd box them first, to get started, and then some unboxed versions could be supported later on where possible). Considering the mechanisms out there in languages that are somewhat similar to Dart, the notion of an // Emulate the SML type `datatype 'a List = Nil | Cons of ('a * 'a List)`
// using names that are Darty, based on an extension of the Dart `enum`
// mechanism with associated data.
enum EnumList<X> {
Nil,
Cons(X head, EnumList<X> tail),
}
int length<X>(EnumList<X> l) {
switch (l) {
case Nil: return 0;
case Cons(var head, var tail): return 1 + length(tail);
}
}
main() {
MyList<int> l = MyList.Cons(42, MyList.Nil);
print(length(l));
} The enum declaration would then be syntactic sugar for a set of class declarations, one declaration similar to the one which is the specified meaning of the enum class today, and one subclass for each value declared in the enum, carrying the declared associated data in final fields. We'd want generated code to do all the usual stuff for value-like entities (starting with operator There will be lots of proposals for better syntax (e.g., for introduction of patterns introducing fresh names to bind as well as fixed values to check for), but the main point is that the notion of an enum mechanism with associated values would be a quite natural way in Dart to express a lot of the properties that algebraic data types are known to have in other languages. |
There are a lot of people in this thread with much more formal computer science education than I possess. My argument will be thus be quite simple. Union types in TypeScript make my life easy as a developer. Not having them in Dart makes my life considerably worse as a developer. That's all I have to share as a consumer of Dart, and quite frankly, that's all the experience I need to have an opinion on whether we should have them or not. I won't be able to provide anything of great intelligence to this conversation, just my opinion based on experience of building things with Dart. How that gets implemented is well outside of my wheelhouse, I'll defer to the software engineers to use (or not use) my feedback. 😄 |
@lifranc Please don't come to language repository just to leave comments that do not add to the conversation. I'm watching the repository so I can keep track of all the conversations happening here... constructive conversations. Your comment is redundant, and unconstructive. |
https://github.com/spebbe/dartz from @spebbe can be another use case that can make use of union types
Also, https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send API, when implemented in Dart, is quite tricky, either we have to have |
I'd be really disappointed if Dart added ADTs / enum types instead of real union types. The latter are much more ergonomic, flexible and capture exactly the essence of what the developer wants to express: Different types are possible. This essence is captured with minimal syntax: It's always annoying if you can't restrict to a subset and always have to deal with the whole enum and match against all of cases and raise an exception for the disallowed cases at runtime instead of having them disallowed at compile-time. It also makes working with types from different packages annoying because you can't easily mix them (esp. individual cases of the enum). This happens often enough to be an annoyance in Swift, Kotlin and other popular languages. After all, we're doing this because we want compile-time checks. That's the whole point of union types. If you've ever worked with TypeScript's unions, as an end-developer (vs. the PL designer), you really don't want to go back to something as inflexible and verbose and unnecessarily complicated as enums. Yeah, enums might be easier to optimize, but I believe we can still find a "good enough" optimization for union types. The use-cases for unions in Dart would be different than those in e.g. Haskell because we mostly use classes and I believe the performance is less of an issue here. For example, we don't represent our lists as Cons | Nil. We already have a List class for that. For more context, please take a look at my quick proposal from a year ago: https://github.com/dart-lang/sdk/issues/4938#issuecomment-396005754 |
@wkornewald The untagged unions concept sounds nice, but it doesn't sound like a replacement to the algebraic data types. They can totally coexist and serve different needs. |
@werediver Sure they could coexist, but what is missing from union types that is absolutely necessary from a practical point of view? Do you have a code example? |
That is quite easy to answer: extra semantics. data Message
= Info String
| Warning String
| Error String Which would be possible with sealed classes and untagged unions together, but that would be noticeably less succinct. |
I definitely agree that union types expressed as Technically speaking, sealed classes don't bring any new feature, they just reduce the boilerplate. For example, sealed classes cannot be used to represent JSON since they are a new type. typedef Json = String | num | List<Json> | Map<String, Json> Instead of such sealed class: sealed class LoginResponse {
data class Success(val authToken) : LoginResponse()
object InvalidCredentials : LoginResponse()
object NoNetwork : LoginResponse()
data class UnexpectedException(val exception: Exception) : LoginResponse()
} we could use a typedef & unions (+ potentially data classes to describe typedef LoginResponse =
| Token
| NoNetwork
| InvalidCredentials
| Exception |
A downcast is certainly also a mechanism that relies on the run-time value of a type, that is, code that performs a downcast is not parametric. Given In other words, parametricity is a strong constraint, especially in a language like Dart where reliance on the run-time value of a type can arise in so many ways. As an aside, here's a positive spin on that: Look how many cool things Dart can do because it has reified type arguments! All those non-parametric usages would just be impossible (or completely type-unsafe, like an Unchecked Cast in Java) if we did not have this kind of reification. The other side of the coin is that it does cause real work to be performed at run time: Space must be allocated to hold the value of all those type parameters, time must be spent computing and passing them, and so on. Extension types allow us to get a kind of type arguments that are guaranteed to be eliminated in specific ways (for example, That's the whole point of extension types: We do not want to pay for a wrapper object, and this implies that the run-time value of the extension type must be the representation type (there's no way a However, if a union type like What the types in It is true that you can destroy this kind of "goodness" by introducing a dynamic type operation (e.g., a cast). This might be perfectly OK for a But the assumption is that the ability to maintain a discipline based on static type checks is worth more than nothing, especially when it costs nothing at run time. Besides, you can always provide manually coded improvements, like the By the way, it wouldn't be hard to create a variant of extension type Union2<X, X1 extends X, X2 extends X>._(X value) {
Union2.in1(X1 this.value);
Union2.in2(X2 this.value);
bool get isValid => value is X1 || value is X2;
X1? get as1OrNull => value is X1 ? value as X1 : null;
X2? get as2OrNull => value is X2 ? value as X2 : null;
X1 get as1 => value as X1;
X2 get as2 => value as X2;
bool get is1 => value is X1;
bool get is2 => value is X2;
R split<R>(R Function(X1) on1, R Function(X2) on2) {
var v = value;
if (v is X1) return on1(v);
if (v is X2) return on2(v);
throw InvalidUnionTypeException(
"Union2<$X1, $X2>",
value,
);
}
R? splitNamed<R>({
R Function(X1)? on1,
R Function(X2)? on2,
R Function(Object?)? onOther,
R Function(Object?)? onInvalid,
}) {
var v = value;
if (v is X1) return (on1 ?? onOther)?.call(v);
if (v is X2) return (on2 ?? onOther)?.call(v);
if (onInvalid != null) return onInvalid(v);
throw InvalidUnionTypeException(
"Union2<$X1, $X2>",
value,
);
}
} This means that every union type is represented by a common supertype ( I didn't do that, because that's yet another chore that users must think about when they are using these union types. In any case, it's an option which is easily expressible if anybody wants it. Finally, let's return to the example: import 'package:extension_type_unions/extension_type_unions.dart';
class Example<T> {
T? value; // Assignments to `value` are not parametric in `T`.
}
void main() {
Example<Object?> example = Example<Union2<String, bool>>(); // Upcast, accepted.
example.value = 42; // The dynamic check uses the representation type and succeeds.
} This is again an example where a type parameter is used in a way which is not parametric: The setter which is implicitly induced by the declaration On the other hand, if you avoid the abstraction step which is implied by using an extension type as the value of an actual type argument then you can get the expected type check statically: import 'package:extension_type_unions/extension_type_unions.dart';
class Example {
Union2<String, bool>? value;
}
void main() {
var x = Example();
// x.value = 42; // Compile-time error, also for `42.u21` and `42.u22`.
x.value = 'foo'.u21; // OK.
x.value = true.u22; // OK.
x.value = null; // OK.
} |
For what it's worth, extension methods cover this nicely. extension<T> on Union2<T, T> {
T get value;
}
Union2<int, double> union;
union.value; // typed as "num" With this, Users don't need to specify a generic argument for that. That's a pattern I've used in a previous experiment based around functions (union) |
That's cool! import 'package:extension_type_unions/extension_type_unions.dart';
extension<T> on Union2<T, T> {
T get lubValue => this as T; // Can't use the name `value`, `Union2` has that already.
}
class A {}
class B1 implements A {}
class B2 implements A {}
void main() {
Union2<int, double> union = 20.u21;
union.lubValue.expectStaticType<Exactly<num>>; // OK.
Union2<B1, B2> classUnion = B2().u22;
classUnion.lubValue.expectStaticType<Exactly<A>>; // OK.
}
typedef Exactly<X> = X Function(X);
extension<X> on X {
X expectStaticType<Y extends Exactly<X>>() => this;
} However, this still doesn't equip the For example, we could add the following to Union2<Never, Symbol> badUnion = false as Union2<Never, Symbol>;
badUnion.lubValue; // Throws. It is possible to determine that the union value is invalid because it throws when we run In contrast, a const something = true;
T fn<T>() => something as T;
void main() {
var x = fn<Union2<num, int, double>>(); // Throws.
} This means that we can use the extra type argument to specify a type which is reified at runtime, and which serves as an approximation of the union type itself. This extra type argument must be an upper bound of the arguments that are the operands of the union. In this case we use So this does matter at run time. It's not nice to have to invent and specify that extra type argument to |
In my day to day work this is one of the features I miss the most in Dart. I understand it would be a substantial undertaking, but it would be really nice to have this. |
another example of use case where this feature would be valuable does this issue being absent from the language funnel mean that is not under the radar? |
It would be nice to have this functionality. Mainly for defining function types, like.
|
A place where union types would be super useful is js-interop. In the JS world a lot of APIs are using them. external void setCenter(JSAny /*LatLng|LatLngLiteral*/ latlng); |
Would it make sense to implement the JS API using two Dart methods for the same JS method? Something like: @JS(...)
class Whatnot {
@JS()
external void setCenter(LatLng latLng);
@JS("setCenter")
external void setCenterJson(Map<String, Object?> latLngJson);
} (I won't expect Dart to statically check that the map has Or another example: @JS('Date')
extension type JSDate._(JSObject _) {
@JS('Date')
external JSDate(int year, Month month,
[int day, int hour, int minute, int second, int milliseconds]);
@JS('Date')
external JSDate.fromMS(int msSinceEpoch);
@JS('Date')
external JSDate.fromString(String dateText);
// ...
} That is: The Dart way to have overloading is to have different names, the JS is to dynamically inspect the arguments. A Dart adaption of a JS API could have multiple names for APIs that accept multiple distinct argument signatures. (Doesn't scale to a function taking ten "String|int" arguments, obviously.) |
@lrhn the problem is that you need to manually give names to stuff. Basic JS world is already huge |
@lrhn having several method names could work for parameters (if the number of combination is not huge) but the problem for return type is still there. |
@a14n I think this is a great example which shows that naively translating APIs does not make sense. Consider for example this type So you can translate this API without union types to Dart:
This makes API clean and Dart-y. |
For sure that's what I did (hide LatLngLiteral) on the current version of google_maps. extension type DirectionsRequest._(JSObject _) implements JSObject {
external DirectionsRequest({
JSAny /*string|LatLng|Place|LatLngLiteral*/ destination,
JSAny /*string|LatLng|Place|LatLngLiteral*/ origin,
TravelMode travelMode,
bool? avoidFerries,
bool? avoidHighways,
bool? avoidTolls,
DrivingOptions? drivingOptions,
String? language,
bool? optimizeWaypoints,
bool? provideRouteAlternatives,
String? region,
TransitOptions? transitOptions,
UnitSystem? unitSystem,
JSArray<DirectionsWaypoint>? waypoints,
});
external JSAny /*string|LatLng|Place|LatLngLiteral*/ destination;
external JSAny /*string|LatLng|Place|LatLngLiteral*/ origin;
external TravelMode travelMode;
external bool? avoidFerries;
external bool? avoidHighways;
external bool? avoidTolls;
external DrivingOptions? drivingOptions;
external String? language;
external bool? optimizeWaypoints;
external bool? provideRouteAlternatives;
external String? region;
external TransitOptions? transitOptions;
external UnitSystem? unitSystem;
external JSArray<DirectionsWaypoint>? waypoints;
} What would you do here? |
I'd drop If none of those are possible, I'd start considering why the two types are even related. In this case, I think it'd be a (Would also give one or both of them a Doesn't work for the accessors. I'd consider writing a Dart wrapper to return a @JS("destination")
external JSObject _destination;
Place get destination {
var destination = _destination;
if (destination is JSString) return Place.parse(destination.toDart);
// how to detect other JS types ...
return place;
}
set destination(Place place) { _destination = place; } That is: Make the Dart API different from the JS API. You'll have to decide whether the goal is to provide a semi-transparent Dart interface to the JS API, or a good Dart API for the same functionality. The latter takes Dart design work. The former isn't always type-safe today. |
Checking extension type DirectionsRequest._(JSObject _) implements JSObject {
external DirectionsRequest({
Place destination,
Place origin,
// ...
});
external Place destination;
external Place origin;
} I think this just continues to show that union types are almost always a symptom of poor API design - an API that tries to cater too much to "make call-sites a bit easier to write" kind of thinking. |
Last week I rewrote a HTML/CSS/JS app in Flutter and I spent way too much time modeling unions, because the client and server were sharing code.
Aren't you making the API less easy to use? 'Street ABC' can be a valid destination argument and LatLngLiteral can be a record typedef. Other API changes would be required, because the code already exists in Google Maps docs, and we would end up with a not more convenient API (less convenient?).
Isn't the compiler's job to avoid this complexity? By introducing an anonymous supertype (the union).
Rewriting the API is a burden, but who is going to maintain the code later? |
Yes. Or at least less convenient ("easy to use" can be easy to write code for, and easy to use correctly, which is not always the same thing). There is no trivial mapping of a JavaScript or TypeScript API to Dart. The type systems are so different that an automatic mapping into Dart is always going to be dynamically typed. If the goal is to have an automatic conversion from JS/TS APIs to Dart APIs with the same behavior and same type safety, then it's currently very much a square peg in a round hole-problem. You have to either lose typing or add more methods. The question here is what are we willing to add to Dart to make JS interop easier and more direct? Do we want to add some (but not all) of the type features of TypeScript to Dart, so that JS APIs can be approximated more closely by Dart types. And if so, how many. This issue is about union types, but it's not just union of Dart types, it's also unions of value types like If there are sufficiently good workarounds for the API mismathching, then matching JS APIs directly is won't count as strongly for adding new features. What we're looking for here is such a workaround. And the question becomes whether the workaround is sufficently good. (It's probably not, it's just how I'd do the API if I wrote it in Dart.) We're definitely not going to add features to Dart only to support JS interop. That's too specialized. A language feature should have some possible general use, and should be able to be defined independendtly of JS interop, because the feature is there for people who never interoperate with JS too. If the goal is to expose precisely the JS API with the TS type system, then nothing short of the TS type system can do that.
We can do union types, probably with intersection types, but it's not something we strongly want for Dart itself. Will it be enough for JS interop, or will we just start needing literal types then? |
Unrelated to places and latitudes: Union types are convenient. Instead of defining N constructors in a class A: |
The Additionally, if API only accepts a With ADT this can be straightforwardly modelled to be always valid and easy to use.
I can't say I agree with that. They map to logic nicely. Trying to model a similar concept with classical OOP is usually clumsy and often looks like a case of, "if you have a hammer, everything looks like a nail". |
Can literal types be separated from this issue? I believe it's conflicting opinions, I personally don't want it, and it's not mentioned in the issue description. I don't think it's desired to have many TS features in Dart. To narrow down, the issue description mentions union types, enhanced switch (which we already have?) and recursive typedefs (should be issue dart-lang/linter#3714?). Intersection types are very related to this issue, but they might be too much to ship with union types (maybe union-types-later?). I wonder if the record spreading feature is related to intersection types. I have a different usage example for Flutter. Currently, the |
the typings package already does most of this job. It tries to clean up the JS/TS API to the most complex object when it encounters unions or method overloads. the package does most of the work that you need to do when you use Darts default JS Interop, so you can just use it instead. a help to make it a little bit easier to use would be very nice tho |
I normally wouldn't want sum|union types The thing is, we have to. Its the only way to non-breakingly strongly type places that use dynamic to accept unrelated types (like for Json) There is no way to fix that without some way to relate types together. That means, we need union types, or rust-like traits (since you can implement Foo for Bar, you can effectively create a shared super type) The benefit of unions is that they are anonymous, and you can even promote it through type checks, as A|B|C could become A|B if we have a guard against C like we do for null. (The benefit of trait-likes is that we don't actually need a new kind of type - just a way to declare implementers outside our control. The main issue for this though, is that |
flutter.dev/go/using-widget-state-property describes a messy problem that the Flutter team is facing with both Material and Cupertino widgets. If union types were a Dart language feature, they would enable an elegant solution. |
Can you give an example? I'm not aware of a use case that could be solved with untagged unions but not by tagged unions. Or is it just an ergonomic problem? |
@mateusfccp if you're aware of a way to obtain the functionality proposed in flutter/flutter#154197 without union types, please let me know 🥺 Edit:
Yes indeed 💯 |
One approach would be implicit coercions. If we had "implicit constructors", possibly extension constructors: static extension Color2Property on WidgetProperty<Color> {
implicit factory WidgetProperty.fromColor(Color color) =
ColorWidgetProperty;
}
static extension Property2Color on Color {
implicit factory Color.fromProperty(
WidgetProperty<Color> color) => color.value;
} Then you could automatically convert between |
Implicit constructors would be awesome; I believe they'd work for sealed class Property<T> {
implicit const factory Property.fromValue(T value) = ValueProperty<T>;
T resolve(Set<WidgetState> states);
}
class ValueProperty<T> implements Property<T> {
const ValueProperty(this.value);
final T value;
@override
T resolve(Set<WidgetState> states) => value;
}
abstract class WidgetStateProperty<T> implements Property<T> {
// no changes necessary for existing WidgetStateProperty API
} And then any parameter with the Overall, I like the idea of union types more (probably much more intuitive for some people, and no additional keyword is needed), but either one would work beautifully. |
I'd like to import some features from functional languages like OCaML and F#.
The main feature I'm proposing is the possibility of using
typedef
to define sum types. They can be thought of as type enumerations.This will, in some cases, reduce the need for
dynamic
making for sounder typing.For example
json.decode
would no longer returndynamic
but JSON a sum type, something like:Sum types could power a new behaviour of switch case that resolves the type based on the typed enumerated. Kind of like
assert
andif
does with contemporaryis
syntax.A better syntax for this type of switch case might be of order, maybe something like dart-lang/sdk#57173.
This would be a powered down version of OCaML and F#
match <arg> with
as I've not included a proposition for type deconstruction, which would probably require tuples (or more in general product types) as discussed in dart-lang/linter#68.The text was updated successfully, but these errors were encountered: