8. Class augmentations

Many dynamic languages support the ability to extend existing classes by adding new methods to them. You may think of categories in Objective-C and Groovy, or open classes in Ruby.

This is generally implemented by providing meta-classes. When some piece of code adds a method foo to, say, SomeClass, then all instances of SomeClass get that new foo method. While very convenient, such an open system may lead to well-known conflicts between the added methods.

Golo provides a more limited but explicit way to add methods to existing classes in the form of class augmentations.

8.1. Wrapping a string with a function

Let us motivate the value of augmentations by starting with the following example. Suppose that we would like a function to wrap a string with a left and right string. We could do that in Golo as follows:

function wrap = |left, str, right| -> left + str + right

# (...)
let str = wrap("(", "foo", ")")
println(str) # prints "(abc)"

Defining functions for such tasks makes perfect sense, but what if we could just add the wrap method to all instances of java.lang.String instead?

8.2. Augmenting classes

Defining an augmentation is a matter of adding a augment block in a module:

module foo

augment java.lang.String {
  function wrap = |this, left, right| -> left + this + right
}

function wrapped = -> "abc": wrap("(", ")")

More specifically:

  1. a augment definition is made on a fully-qualified class name, and
  2. an augmentation function takes the receiver object as its first argument, followed by optional arguments, and
  3. there can be as many augmentation functions as you want, and
  4. there can be as many augmentations as you want.

It is a good convention to name the receiver this, but you are free to call it differently.

Also, augmentation functions can take variable-arity arguments, as in:

augment java.lang.String {

  function concatWith = |this, args...| {
    var result = this
    foreach(arg in args) {
      result = result + arg
    }
    return result
  }
}

# (...)
function varargs = -> "a": concatWith("b", "c", "d")

It should be noted that augmentations work with class hierarchies too. The following example adds an augmentation to java.util.Collection, which also adds it to concrete subclasses such as java.util.LinkedList:

augment java.util.Collection {
  function plop = |this| -> "plop!"
}

# (...)
function plop_in_a_list = -> java.util.LinkedList(): plop()

8.3. Augmentation scopes, reusable augmentations

By default, an augmentation is only visible from its defining module.

Augmentations are clear and explicit as they only affect the instances from which you have decided to make them visible.

It is advised to place reusable augmentations in separate module definitions. Then, a module that needs such augmentations can make them available through imports.

Suppose that you want to define augmentations for dealing with URLs from strings. You could define a string-url-augmentations.golo module source as follows:

module my.StringUrlAugmentations

import java.net

augment java.lang.String {

  function toURL = |this| -> URL(this)

  function httpGet = |this| {
    # Open the URL, get a connection, grab the body as a string, etc
    # (...)
  }

  # (...)
}

Then, a module willing to take advantage of those augmentations can simply import their defining module:

module my.App

import my.StringUrlAugmentations

function googPageBody = -> "http://www.google.com/": httpGet()

Tip

As a matter of style, we suggest that your module names end with Augmentations. Because importing a module imports all of its augmentation definitions, we suggest that you modularize them with fine taste (for what it means).

8.4. Standard augmentations

Golo comes with a set of pre-defined augmentations.

Caution

Until we have a tool to extract the documentation from Golo source files, their documentation will simply be the inclusion of that file content.

# ............................................................................................... #
#
# Copyright 2012-2013 Institut National des Sciences Appliquées de Lyon (INSA-Lyon)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# ............................................................................................... #

module gololang.StandardAugmentations


local function _newWithSameType = |this| {
  try {
    return this: getClass(): newInstance()
  } catch (e) {
    if not(e oftype java.lang.InstantiationException.class) {
      throw e
    }
    let fallback = match {
      when this oftype java.util.RandomAccess.class then java.util.ArrayList()
      when this oftype java.util.List.class then java.util.LinkedList()
      when this oftype java.util.Set.class then java.util.HashSet()
      when this oftype java.util.Map.class then java.util.HashMap()
      otherwise null
    }
    if fallback is null {
      raise("Cannot create a new collection from " + this: getClass())
    }
    return fallback
  }
}

local function _closureWithIndexArgument = |target| -> match {
  when target: type(): parameterCount() == 0
    then java.lang.invoke.MethodHandles.dropArguments(target, 0, java.lang.Object.class)
  otherwise
    target
}

# ............................................................................................... #

augment java.lang.Number {

  function times = |count, func| {
    let target = _closureWithIndexArgument(func)
    for (var i = 0, i < count, i = i + 1) {
      target(i)
    }
  }

  function upTo = |low, high, func| {
    let target = _closureWithIndexArgument(func)
    for (var i = low, i <= high, i = i + 1) {
      target(i)
    }
  }

  function downTo = |high, low, func| {
    let target = _closureWithIndexArgument(func)
    for (var i = high, i >= low, i = i - 1) {
      target(i)
    }
  }
}

# ............................................................................................... #

augment java.lang.invoke.MethodHandle {

  function to = |this, interfaceClass| -> asInterfaceInstance(interfaceClass, this)

  function andThen = |this, filter| ->
    java.lang.invoke.MethodHandles.filterReturnValue(this, filter)

  function bindAt = |this, pos, val| ->
    java.lang.invoke.MethodHandles.insertArguments(this, pos, val)
}

# ............................................................................................... #

augment java.lang.String {

  function format = |this, args...| {
    if args: length() == 1 {
      return java.lang.String.format(this, args: get(0))
    } else {
      return java.lang.String.format(this, args)
    }
  }

  function toInt = |this| ->
    java.lang.Integer.parseInt(this)

  function toInteger = |this| ->
    java.lang.Integer.parseInt(this)

  function toDouble = |this| ->
    java.lang.Double.parseDouble(this)

  function toFloat = |this| ->
    java.lang.Float.parseFloat(this)

  function toLong = |this| ->
    java.lang.Long.parseLong(this)
}

# ............................................................................................... #

augment java.lang.Iterable {

  function reduce = |this, initialValue, func| {
    var acc = initialValue
    foreach (element in this) {
      acc = func(acc, element)
    }
    return acc
  }

  function each = |this, func| {
    foreach (element in this) {
      func(element)
    }
    return this
  }

}

# ............................................................................................... #

augment java.util.Collection {

  function newWithSameType = |this| -> _newWithSameType(this)

}

# ............................................................................................... #

augment java.util.List {

  function append = |this, element| {
    this: add(element)
    return this
  }

  function prepend = |this, element| {
    this: add(0, element)
    return this
  }

  function insert = |this, index, element| {
    this: add(index, element)
    return this
  }

  function append = |this, head, tail...| {
    this: append(head)
    foreach (element in tail) {
      this: append(element)
    }
    return this
  }

  function prepend = |this, head, tail...| {
    for (var i = tail: length() - 1, i >= 0, i = i - 1) {
      this: prepend(tail: get(i))
    }
    return this: prepend(head)
  }

  function head = |this| -> this: get(0)
  function tail = |this| -> this: subList(1, this: size())
  function unmodifiableView = |this| -> java.util.Collections.unmodifiableList(this)

  function find = |this, pred| {
    foreach (element in this) {
      if pred(element) {
        return element
      }
    }
    return null
  }

  function filter = |this, pred| {
    let filtered = this: newWithSameType()
    foreach (element in this) {
      if pred(element) {
        filtered: append(element)
      }
    }
    return filtered
  }

  function map = |this, func| {
    let mapped = this: newWithSameType()
    foreach (element in this) {
      mapped: append(func(element))
    }
    return mapped
  }

  function join = |this, separator| {
    var buffer = java.lang.StringBuilder("")
    if not (this: isEmpty()) {
      buffer: append(this: head())
      let tail = this: tail()
      if not (tail: isEmpty()) {
        buffer: append(separator)
        buffer: append(tail: join(separator))
      }
    }
    return buffer: toString()
  }

  function reverse = |this| {
    java.util.Collections.reverse(this)
    return this
  }

  function reversed = |this| {
    let reversedList = this: newWithSameType()
    reversedList: addAll(this)
    return reversedList: reverse()
  }

  function order = |this| {
    java.util.Collections.sort(this)
    return this
  }

  function ordered = |this| {
    let sortedList = this: newWithSameType()
    sortedList: addAll(this)
    return sortedList: order()
  }

  function order = |this, comparator| {
    java.util.Collections.sort(this, comparator)
    return this
  }

  function ordered = |this, comparator| {
    let sortedList = this: newWithSameType()
    sortedList: addAll(this)
    return sortedList: order(comparator)
  }
}

# ............................................................................................... #

augment java.util.Set {

  function include = |this, element| {
    this: add(element)
    return this
  }

  function exclude = |this, element| {
    this: remove(element)
    return this
  }

  function include = |this, first, rest...| {
    this: add(first)
    foreach (element in rest) {
      this: add(element)
    }
    return this
  }

  function exclude = |this, first, rest...| {
    this: remove(first)
    foreach (element in rest) {
      this: remove(element)
    }
    return this
  }

  function has = |this, element| -> this: contains(element)

  function has = |this, first, rest...| {
    if not(this: contains(first)) {
      return false
    } else {
      foreach (element in rest) {
        if not(this: contains(element)) {
          return false
        }
      }
    }
    return true
  }

  function unmodifiableView = |this| -> java.util.Collections.unmodifiableSet(this)

  function find = |this, pred| {
    foreach (element in this) {
      if pred(element) {
        return element
      }
    }
    return null
  }

  function filter = |this, pred| {
    let filtered = this: newWithSameType()
    foreach (element in this) {
      if pred(element) {
        filtered: include(element)
      }
    }
    return filtered
  }

  function map = |this, func| {
    let mapped = this: newWithSameType()
    foreach (element in this) {
      mapped: include(func(element))
    }
    return mapped
  }
}

# ............................................................................................... #

augment java.util.Map {

  function add = |this, key, value| {
    this: put(key, value)
    return this
  }

  function delete = |this, key| {
    this: remove(key)
    return this
  }

  function addIfAbsent = |this, key, value| {
    if not(this: containsKey(key)) {
      if isClosure(value) {
        this: put(key, value())
      } else {
        this: put(key, value)
      }
    }
    return this
  }

  function getOrElse = |this, key, replacement| {
    let value = this: get(key)
    if value isnt null {
      return value
    }
    if isClosure(replacement) {
      return replacement()
    } else {
      return replacement
    }
  }

  function unmodifiableView = |this| -> java.util.Collections.unmodifiableMap(this)

  function newWithSameType = |this| -> _newWithSameType(this)

  function find = |this, pred| {
    foreach (entry in this: entrySet()) {
      let key = entry: getKey()
      let value = entry: getValue()
      if pred(key, value) {
        return entry
      }
    }
    return null
  }

  function filter = |this, pred| {
    let filtered = this: newWithSameType()
    foreach (entry in this: entrySet()) {
      let key = entry: getKey()
      let value = entry: getValue()
      if pred(key, value) {
        filtered: put(key, value)
      }
    }
    return filtered
  }

  function map = |this, func| {
    let mapped = this: newWithSameType()
    foreach (entry in this: entrySet()) {
      let key = entry: getKey()
      let value = entry: getValue()
      let result = func(key, value)
      mapped: put(result: getKey(), result: getValue())
    }
    return mapped
  }

  function reduce = |this, initialValue, func| {
    var acc = initialValue
    foreach (entry in this: entrySet()) {
      let key = entry: getKey()
      let value = entry: getValue()
      acc = func(acc, key, value)
    }
    return acc
  }

  function each = |this, func| {
    foreach (entry in this: entrySet()) {
      func(entry: getKey(), entry: getValue())
    }
    return this
  }
}

# ............................................................................................... #

augment gololang.Tuple {

  function find = |this, pred| {
    foreach (element in this) {
      if pred(element) {
        return element
      }
    }
    return null
  }

  function filter = |this, func| {
    let matching = list[]
    foreach element in this {
      if func(element) {
        matching: add(element)
      }
    }
    return gololang.Tuple.fromArray(matching: toArray())
  }

  function map = |this, func| {
    let values = list[]
    foreach element in this {
      values: add(func(element))
    }
    return gololang.Tuple.fromArray(values: toArray())
  }

  function join = |this, separator| {
    let size = this: size()
    case {
      when size == 0 {
        return ""
      }
      when size == 1 {
        return this: get(0): toString()
      }
      otherwise {
        let buffer = java.lang.StringBuilder(this: get(0): toString())
        for (var i = 1, i < size, i = i + 1) {
          buffer: append(separator): append(this: get(i): toString())
        }
        return buffer: toString()
      }
    }
  }
}

# ............................................................................................... #