Skip to main content

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index] [List Home]
[golo-dev] Augmentation scope

Hi all,

as recently noted by Philippe, the scope of augmentations can be quite
misleading.

Indeed, an augmentation (classical `augment` or named `augment ... with`)
are only visible in the module where it is defined, or if the said
module is imported (see 
http://golo-lang.org/documentation/next/#_augmentation_scopes_reusable_augmentations)

However, while this behavior can be useful to limit the scope of
augmentation and thus have more predictable code (explicit is better
than implicit), it prevents the creation of libraries of polymorphic
functions or reusable mixin-like augmentations.

For instance, if I want to create a library of functions dealing with
sized object:

    module MyLib

    function printSize = |o| { 
        println("The size of " + o + " is " + o: size() + "!")
    }

This function can only be used on object having a “native” `size`
method. For example, given the following module
    
    module MyData

    struct Sized = { size }

    struct NoSize = { val }

    augment NoSize {
      function size = |this| -> this: val()
    }

the `printSize` function has a somewhat unpredictable behavior if I
don't know the underlying implementation of my objects, which is bad for
code encapsulation:

    module Main

    import MyData
    import MyLib

    augment java.lang.String {
      function size = |this| -> this: length()
    }

    function main = |args| {

      let str = "hello"
      let s = Sized(42)
      let ns = NoSize(42)
      let l = list[1, 2, 3]
      let do = DynamicObject(): define("size", |this| -> 42)

      # Ok, it's a native java method
      require(l: size() == 3, "err")
      try {
        printSize(l)
      } catch (e) {
        println("## ERR: " + e)
      }

      # Ok, it's a generated java method
      require(s: size() == 42, "err")
      try {
        printSize(s)
      } catch (e) {
        println("## ERR: " + e)
      }

      # Ok, DynamicObject
      require(do: size() == 42, "err")
      try {
        printSize(do)
      } catch (e) {
        println("## ERR: " + e)
      }

      # Ok, augmented in the module where it is used
      require(str: size() == 5, "err")
      # Fails... MyLib don't know the augmentation
      try {
        printSize(str)
      } catch (e) {
        println("## ERR: " + e)
      }

      # Ok, the module where the struct is augmented is imported 
      # where the method is used
      require(ns: size() == 42, "err")
      # Fails... MyLib don't know the augmentation
      try {
        printSize(ns)
      } catch (e) {
        println("## ERR: " + e)
      }

    }

running this module, we got:

    The size of [1, 2, 3] is 3!
    The size of struct Sized{size=42} is 42!
    The size of gololang.DynamicObject@59a6e353 is 42!
    ## ERR: java.lang.NoSuchMethodError: class java.lang.String::size
    ## ERR: java.lang.NoSuchMethodError: class MyData.types.NoSize::size

while all `require` calls are ok, since everything needed is imported,
calls to `printSize` can fail.

While I'm not sure how to change this, I think it's unfortunate.

I think of 2 solutions:

* make augmentation applications be looked-up in all the modules of the
  call chain (not only where the function is defined but also where it
  is used!). This can be harder than it seems and looks like ES hoisting :)

* add a `augment globally` construct (or `global augment`) that allow
  the augmentation to be taken into account anywhere in the code. I
  don't think it's a good approach either, since it can make realy hard
  to know which methods are available on an object, and since import
  order is important to know which augmentation is taken into account,
  the behavior can be unpredictable in the case of conflicting
  augmentations.

What do you think ?


Back to the top