union 类型(即sum types)在golang语言中的实现

http://www.jerf.org/iri/post/2917

 

Sum Types in Go

posted Jun 02, 2013
in Programming, Golang, Haskell

A couple of months back, I analyzed whether I wanted to propose switching to Go for work. I've still technically got the blog post with the results of that analysis in the pipeline (though who knows when I'll get it up), but there's a part of it that keeps coming up online, and I want to get this bit out faster. It's about whether Go has "sum types".


A sum type is a type in a language that can have multiple different kinds of values, which themselves may contain values of differing types.

To put it in C terms, it is a "union" in a struct with an element that says which member of the union is currently the active element. In fact the Wikipedia page on the topic lumps the two together. Different elements of the union may have significantly different memory sizes, so languages that make heavy use of this will not use a literal C union, but this gets the idea across.

A classic example is an AST type that defines what a legal abstract syntax tree expression is, pulled from the Haskell GADT Wikibook page:

data Expr a where
   I   :: Int  -> Expr Int
   B   :: Bool -> Expr Bool
   Add :: Expr Int -> Expr Int -> Expr Int
   Mul :: Expr Int -> Expr Int -> Expr Int
   Eq  :: Expr Int -> Expr Int -> Expr Bool

This says that an expression may be an I for Int type, which then contains an Int value, or it may be a B that contains a Bool, or it may be an Add or Mul expression which itself may contain some expression of type Int, or an Eq expression. This is all recursive, so Mul (Add (I 3) (I 4)) (I 5), the translation of (3 + 4) * 5 in traditional infix syntax, is legal.

Good languages support making it impossible to confuse the elements, so you have to "deconstruct" the sum type to do anything with it. Generally associated with a type like this you'll see a base set of functions that run the type through a case statement. Using the above, we can evaluate the expression with:

eval :: Expr a -> a
eval expr = case expr of
                (I n) -> n
                (B b) -> b
                (Add e1 e2) -> eval e1 + eval e2
                (Mul e1 e2) -> eval e1 * eval e2
                (Eq  e1 e2) -> eval e1 == eval e2

(I transformed the original into a case statement, for better parallels with Go.)

Sum types are a great example of how learning a different language can expand your mind. If you've never used a language with good support for sum types, they sound worthless; after all, you've made it this far without ever needing explicit support, and you can bodge together something that works if you ever need to. But once you've become fluent in a language that heavily uses them, you'll never stop seeing places where you need them, and you'll miss them badly. Two places where sum types are particularly strong are the aforementioned AST example, and any sort of protocol, where a sum type can easily represent the legal messages that can be received, while excluding by construction any message that can not be received. When this is done, the type system itself helps you correctly handle errors at the outermost level, then pass the messages along to an inner layer that need no longer worry about such basic error handling. It's a good idea anyhow, and having the type system help enforce it is even better.

AST types are presumably forever going to be a marginal case in Go. Nothing stops you from writing a compiler in Go, but it will never be a dominant use case for the language. Protocols, on the other hand, appear everywhere; every chan in Go defines a protocol. Often they are quite simple (chan bool can only be so complicated), but they can't all be that simple. A serious Go program will be steeped in protocols internally.

But, alas, as is well known, Go does not contain union types. Except, as the FAQ entry alludes to, it sort of already does. But the FAQ is a faint allusion, and my Googling can't find anyone demonstrating what I consider the correct way (you can do much better than interface{}, which seems to be the current de facto standard), so let me show it to you. Let's translate that Expression type into Go.

package main

import "fmt"

// Rather than a "type", we declare an "interface" for the type
type Expr interface {
	isExpr()
}

// We can't define an implementation of isExpr on int directly, so
// we wrap it
type I int

// And tag it as an "Expr". This function's only point is to do that.
func (i I) isExpr() {}

// And so on:
type B bool
func (b B) isExpr() {}

type Add struct { left Expr; right Expr }
func (a Add) isExpr() {}

type Mul struct { left Expr; right Expr }
func (m Mul) isExpr() {}

type Eq struct { left Expr; right Expr }
func (e Eq) isExpr() {}

func eval (e Expr) (r Expr) {
    switch exp := e.(type) {
        case I: r = exp
        case B: r = exp
        case Add: r = I(eval(exp.left).(I) + eval(exp.right).(I))
        case Mul: r = I(eval(exp.left).(I) * eval(exp.right).(I))
        case Eq: r = B(eval(exp.left).(I) == eval(exp.right).(I))
    }
    return
}

func main () {
    fmt.Println("4 + 5: ", eval(Add{I(4), I(5)}))
    fmt.Println("(3 + 2) * 2: ", eval(Mul{Add{I(3), I(2)}, I(2)}))
    // you'll never see this output:
    fmt.Println("Runtime error: ", eval(Add{I(3), B(true)}))
}

You can try that on the Go Playground, or if that is ever removed, paste that into a file and run with "go run". (I deliberately condensed the code for blog-readability, hit "Format" on the playground if you want to see the official formatting.)

There are a couple of differences in the translation. eval can not return either a "true" int or a "true" bool (that is, values of the literal Go types int or bool), so we write it to return an Expr unconditionally. We've lost some type safety; Haskell would refuse to compile the third sample output, but Go does, and the type system doesn't prevent eval from returning an Add</

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值