Search code examples
scalafunctional-programmingidioms

What's the idiomatic way to define multiple functions as the same type in Scala?


I am an experienced programmer in ruby, python and javascript (specifically back-end node.js), I have worked in java, perl and c++, and I've used lisp and haskell academically, but I'm brand new to Scala and trying to learn some conventions.

I have a function that accepts a function as a parameter, similar to how a sort function accepts a comparator function. What is the more idiomatic way of implementing this?

Suppose this is the function that accepts a function parameter y:

object SomeMath {
  def apply(x: Int, y: IntMath): Int = y(x)
}

Should IntMath be defined as a trait and different implementations of IntMath defined in different objects? (let's call this option A)

trait IntMath {
   def apply(x: Int): Int
}

object AddOne extends IntMath {
   def apply(x: Int): Int = x + 1
}

object AddTwo extends IntMath {
  def apply(x: Int): Int = x + 2
}

AddOne(1)
// => 2
AddTwo(1)
// => 3
SomeMath(1, AddOne)
// => 2
SomeMath(1, AddTwo)
// => 3

Or should IntMath be a type alias for a function signature? (option B)

type IntMath = Int => Int

object Add {
  def one: IntMath = _ + 1
  def two: IntMath = _ + 2
}

Add.one(1)
// => 2
Add.two(1)
// => 3
SomeMath(1, Add.one)
// => 2
SomeMath(1, Add.two)
// => 3

but which one is more idiomatic scala?

Or are neither idiomatic? (option C)

My previous experiences in functional languages leans me towards B, but I have never seen that before in scala. On the other hand, though the trait seems to add unnecessary clutter, I have seen that implementation and it seems to work much more smoothly in Scala (since the object becomes callable with the apply function).

[Update] Fixed the examples code where an IntMath type is passed into SomeMath. The syntatic sugar that scala provides where an object with an apply method becomes callable like a function gives the illusion that AddOne and AddTwo are functions and passed like functions in Option A.


Solution

  • Since Scala has explicit function types, I'd say that if you need to pass a function to your function, use the function type, i.e. your option B. They are explicitly there for this purpose.

    It is not exactly clear what do you mean by saying that you "have never seen that before in scala", BTW. A lot of standard library methods accept functions as their parameters, for example, collection transformation methods. Creating type aliases to convey semantics of a more complex type is perfectly idiomatic Scala as well, and it does not really matter whether the aliased type is a function or not. For example, one of the core types in my current project is actually a type alias to a function, which conveys the semantics using a descriptive name and also allows to easily pass conforming functions without a necessity to explicitly create a subclass of some trait.

    It is also important, I believe, to understand that syntax sugar of the apply method does not really have anything to do with how functions or functional traits are used. Indeed, apply methods do have this ability to be invoked just with parentheses; however, it does not mean that different types having an apply method, even with the same signature, are interoperable, and I think that this interoperability is what matters in this situation, as well as an ability to easily construct instances of such types. After all, in your specific example it only matters for your code whether you could use the syntax sugar on IntMath or not, but for users of your code an ability to easily construct an instance of IntMath, as well as an ability to pass some existing thing they already have as IntMath, is much more important.

    With the FunctionN types, you have the benefit of being able to use the anonymous function syntax to construct instances of these types (actually, several syntaxes, at least these: x => y, { x => y }, _.x, method _, method(_)). There even was no way to create instances of "Single Abstract Method" types before Scala 2.11, and even there it requires a compiler flag to actually enable this feature. This means that the users of your type will have to write either this:

    SomeMath(10, _ + 1)
    

    or this:

    SomeMath(10, new IntMath {
      def apply(x: Int): Int = x + 1
    })
    

    Naturally, the former approach is much clearer.

    Additionally, FunctionN types provide a single common denominator of functional types, which improves interoperability. Functions in math, for example, do not have any kind of "type" except their signature, and you can use any function in place of any other function, as long as their signatures match. This property is beneficial in programming too, because it allows reusing definitions you already have for different purposes, reducing the number of conversions and therefore potential mistakes that you might do when writing these conversions.

    Finally, there are indeed situations where you would want to create a separate type for your function-like interface. One example is that with full-fledged types, you have an ability to define a companion object, and companion objects participate in implicits resolution. This means that if instances of your function type are supposed to be provided implicitly, then you can take advantage of having a companion object and define some common implicits there, which would make them available for all users of the type without additional imports:

    // You might even want to extend the function type
    // to improve interoperability
    trait Converter[S, T] extends (S => T) {
      def apply(source: S): T
    }
    
    object Converter {
      implicit val intToStringConverter: Converter[Int, String] = new Converter[Int, String] {
        def apply(source: Int): String = source.toString
      }
    }
    

    Here having a piece of implicit scope associated with the type is useful, because otherwise users of Converter would need to always import contents of some object/package to obtain default implicit definitions; with this approach, however, all implicits defined in object Converter will be searched by default.

    However, these situations are not very common. As a general rule, I think, you should first try to use the regular function type. If you find that you do need a separate type, for a practical reason, only then you should create your own type.