简体   繁体   中英

F# limitations of discriminated unions

I am trying to port a small compiler from C# to F# to take advantage of features like pattern matching and discriminated unions. Currently, I am modeling the AST using a pattern based on System.Linq.Expressions: A an abstract base "Expression" class, derived classes for each expression type, and a NodeType enum allowing for switching on expressions without lots of casting. I had hoped to greatly reduce this using an F# discriminated union, but I've run into several seeming limitations:

  • Forced public default constructor (I'd like to do type-checking and argument validation on expression construction, as System.Linq.Expressions does with it's static factory methods)
  • Lack of named properties (seems like this is fixed in F# 3.1)
  • Inability to refer to a case type directly. For example, it seems like I can't declare a function that takes in only one type from the union (eg let f (x : TYPE) = x compiles for Expression (the union type) but not for Add or Expression.Add . This seems to sacrifice some type-safety over my C# approach.

Are there good workarounds for these or design patterns which make them less frustrating?

I think, you are stuck a little too much with the idea that a DU is a class hierarchy. It is more helpful to think of it as data, really. As such:

  • Forced public default constructor (I'd like to do type-checking and argument validation on expression construction, as System.Linq.Expressions does with it's static factory methods)

A DU is just data, pretty much like say a string or a number, not functionality. Why don't you make a function that returns you an Expression option to express, that your data might be invalid.

  • Lack of named properties (seems like this is fixed in F# 3.1)

If you feel like you need named properties, you probably have an inappropriate type like say string * string * string * int * float as the data for your Expression . Better make a record instead, something like AddInfo and make your case of the DU use that instead, like say | Add of AddInfo | Add of AddInfo . This way you have properties in pattern matches, intellisense, etc.

  • Inability to refer to a case type directly. For example, it seems like I can't declare a function that takes in only one type from the union (eg let f (x : TYPE) = x compiles for Expression (the union type) but not for Add or Expression.Add. This seems to sacrifice some type-safety over my C# approach.

You cannot request something to be the Add case, but you definitely do can write a function, that takes an AddInfo . Plus you can always do it in a monadic way and have functions that take any Expression and only return an option . In that case, you can pattern match, that your input is of the appropriate type and return None if it is not. At the call site, you then can "use" the value in the good case, using functions like Option.bind .

Basically try not to think of a DU as a set of classes, but really just cases of data. Kind of like an enum.

You can make the implementation private. This allows you the full power of DUs in your implementation but presents a limited view to consumers of your API. See this answer to a related question about records (although it also applies to DUs).

EDIT

I can't find the syntax on MSDN, but here it is:

type T =
  private
  | A
  | B

private here means "private to the module."

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM