Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Thanks for all the details.

> If you could define methods anywhere, you introduce hidden dependencies and more coupling. A new package might change the nature of the type. This is not acceptable.

This is the Expression Problem: adding new methods to existing types and/or implementing existing methods on new types. The intent is on the contrary to decouple things. You can use any feature to sabotage code.

> What will PrintHex(T(42)) return? "2a", "0x2a", or "0x2A"?

The compiler would warn you and take the latest definition into account, which is the one from "baz". That would be a pragmatic approach.

I notice that neither "bar" nor "baz" import "foo". So in your (hypothetical) example, Hex belongs to the global namespace, not "foo". I guess this is the case in non-hypothetical Go too? I didn't realize this, thanks.



> The compiler would warn you and take the latest definition into account, which is the one from "baz".

As a general rule, the Go compiler does't issue warnings.

There is no latest definition. The order of imports doesn't matter, and bar and baz could be imported by different packages. I imported both in foo here since we only have three packages (four, if you count numbers), but bar and baz could be imported (separately) by qux and quux, and both of them could be imported by package waldo.

> I notice that neither "bar" nor "baz" import "foo".

Correct, they don't need to, but they couldn't anyway, as import cycles are not allowed. Package dependencies need to form an acyclic graph.

> Hex belongs to the global namespace, not "foo". I guess this is the case in non-hypothetical Go too?

No, Hex belongs to Foo. If Hex were to be used by package fred, fred would have to import foo, then use foo.Hex.

In fact, in this case, I should have made hex an unexported interface (lower case). This is a very common idiom in Go. Users of the language define internal interfaces to group other, 3rd party types, by behavior. They can then execute this behavior in a type-safe manner while having all the 3rd party types all decoupled from themselves and from your own package.

The fact that you don't have to declare interfaces before defining the types is the number one reason Go is a statically-typed language that feels like a dynamically-typed language.

You mentioned the Expression Problem. Haskell solves this by type classes. Go interfaces are dual to type classes. There is no need to add methods to existing types, as the consumer can define new types that embed the old type and its method. And if some other code uses interfaces, it will then be able to use both the old type and your new type.

    package europe
    
    import "fmt"
    
    type Celsius float64
    
    func (c Celsius) Kelvin() float64 { return c + 273.15 }
    
    func (c Celsius) String() { return fmt.Sprinf("%d°C", c) }
You can print Celsius(42) and you'll get 42°C, and you can pass the type to some function consuming a Kelvin interface:

    package physics
    
    import "europe"
    
    type Temperature interface {
    	Kelvin() float64
    }
    
    func ThermalExpansion(m Material, t Temperature) float64 {
    	... // some code
    	x := t.Kelvin() * m.ThermalCoefficient()
    	... // some more code
    	return x
    }
But then if you write an american package:

    package america
    
    import "europe"
    
    type Fahrenheit struct {
    	europe.Celsius
    }
    
    func (f Fahrenheit) String() { return fmt.Sprinf("%d°F", f.Celsius * 9.0/5.0 + 32.0) }
You can print in °F, and you can still pass american.Fahrenheit to physics.ThermalExpansion.

    t := america.Fahrenheit{42}
    fmt.Print(t) // will print in °F
    x := physics.ThermalExpansion(Steel, t) // no problem


> Correct, they don't need to, but they couldn't anyway, as import cycles are not allowed. Package dependencies need to form an acyclic graph.

I was working with those dependencies in mind:

    foo -> {fmt, numbers}  // defines foo.Hex
    bar -> foo             // impl. foo.Hex (T)
    baz -> foo             // impl. foo.Hex (T)
    main -> {bar, baz}     // conflict detected
But now I understand that you declare Hex in both bar and baz independently of foo. The reason I wrote about a global namespace is because bar.Hex and baz.Hex are implicitly implementing foo.Hex thanks to their names (and signature), whereas in other languages foo.Hex, bar.Hex and baz.Hex would be considered as distinct methods. But the confusion is cleared now, thanks.

The example with temperatures is interesting because defining conversion methods is a recurrent problem and you missed one case that is useful too. The European author of the Celsius package doesn't know about Farenheit, or simply doesn't care about it. However, the author of the america package knows about Celsius and needs to convert between Celsius and Farenheit. He could define a ToF method which converts from °C to °F:

    func (c Celsius) ToF() Farenheit { ... }
The same could be done with other units: Rankine, Delisle, Newton, ... (thanks Wikipedia)

Now in Go, you don't declare methods like this. You have most likely:

    func KtoF (k Kelvin)  Farenheit { ... }
    func CtoF (c Celsius) Farenheit { ... }
    func RtoF (r Rankine) Farenheit { ... }
    func DtoF (d Delisle) Farenheit { ... }
    func NtoF (n Newton)  Farenheit { ... }
... each of them called from a manual dispatch:

    func ToF (i interface{}) Kelvin { switch i.(type) ... }
Embedding seems difficult when you don't own the data (e.g. a library creates a whole graph of nodes made of its own types).




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: