3

I'm new to Scala. I'd like to have a class which expresses something like coordinates measured in positive integers. (This is a simplified example; please don't point to the existence of other coordinate classes; assume that I really need a new class.)

My first attempt was to simply put require(x >= 0 && y >= 0) into the class, but members of my team did not like that this would throw exceptions.

My second attempt returns an Option[Coordinate]. So now every time I create such a coordinate, I have 4 lines where I would normally have just one. Worse, the Optionality infects everything I'm building out these coordinates. Now I have an Option[Row], Option[Rectangle], Option[Cube], Option[Report]...

Other people have advised me to fix the problem by making a Natural number type which would always be positive. But now this pushes the issue to creating a Natural out of a regular integer that might be negative.

I'm new to Scala, but it seems to me that Options, Either, and Try make sense if

(a) the operation can fail, because you're doing I/O

(b) there's a reasonable alternative to the absence of something

It seems that neither of these capture what I'm trying to do here, which is really to guard against someone making a mathematical mistake in their coding.

Advice?

NeilK
  • 778
  • 1
  • 6
  • 15

2 Answers2

1

it seems to me that Options, Either, and Try make sense if

The classes you've mentioned are each responsible for a certain effect. You're right in describing them in some context where an effect is needed, like doing IO, or interacting with a method which may throw an exception (possibly a Java API) or wanting to describe a chain of operations where one or more may fail.

I think it boils down to how deep down the rabbit hole you want to go. What I mean by that is that the power of Scala is its type system, and that is what you can and should leverage to your advantage. I agree with you that using an Option[Coordinate] doesn't feel natural, it certainly doesn't to me. The question that immediatly arises is "How can I leverage the type system to help me only accept natural numbers?". Well, it seems that other people have had the same concern as you.

For example, there is a library called shapeless which has under its belt a Nat type representing natural numbers. For example, given the following case class:

import shapeless.Nat

case class Coordinate(x: Nat, y: Nat)

If I create it with natural numbers, everything compiles:

def main(args: Array[String]): Unit = {
  val cord = Coordinate(1, 1)
}

But once I give it a non natural number, the compiler complains!

def main(args: Array[String]): Unit = {
  val cord = Coordinate(-1, 1)
}

With:

Error:(13, 28) Expression -1 does not evaluate to a non-negative Int literal
   val cord = Coordinates(-1, 1)

IMO that is the power of the type system which you should take to your advantage. If you and your team are willing to put the effort to encode these types properly, you can give your team and users a very strong guarantee of "if this compiles, it does what you mean it to do".

Yuval Itzchakov
  • 146,575
  • 32
  • 257
  • 321
  • 1
    This looks a lot more, um, natural. :) But I'm leery of introducing large libraries that radically change expectations. I see things in there about 'backtracking types' which sounds cool and everything, but I'm not trying to create puzzles for my colleagues. If the answer is actually "you can't do that in standard Scala" then I can accept that. – NeilK Jul 31 '17 at 00:38
  • @NeilK You can adapt it to the level you feel comfortable. Using shapeless was just an example, there are many things you can do yourself at the language level. Thats why I said its up to you and your colleagues to decide how far you take it. – Yuval Itzchakov Jul 31 '17 at 03:38
  • Shapeless' Nat type is probably not suitable for this, as using numbers larger than 400 make the compiler explode, and numbers larger than 50 make it extremely slow; but a Coordinate type could routinely see numbers much higher than 400. See https://stackoverflow.com/a/32888393/615987 – Patrick Mar 24 '20 at 09:59
0

You have a very good question.

My first attempt was to simply put require(x >= 0 && y >= 0) into the class, but members of my team did not like that this would throw exceptions.

That's right. If you choose this way you have to deal with the exception somehow.

I'm new to Scala, but it seems to me that Options, Either, and Try make sense if

(a) the operation can fail, because you're doing I/O

(b) there's a reasonable alternative to the absence of something

That's half-true. If the invariants of your domain says that some element could be empty, then you can use Option to express that behaviour and also you're gaining expresiveness. The use of Option, specially, isn't constrained to I/O operations.

Before I address your (b) concern, let me explain something.

Scala has contexts (I won't use the 'M-word' here) that represents an effect, as @Yuval Itzchakov said before. That is: you have a value that is wrapped with something that offers some combinators and also offers some behaviour handling. Either, Try and Option are examples of that and when you choose to use them you're commiting to handle your data wrapped in these contexts. The effect of them is that you have to handle that effect.

Remember than an effect is something that can occur with your data: an exception (Try, Either), a null value (Option), an asynchronous computation (Future) and so on. Your algebra will be encoded in term of these contexts because it will help you to handle that expected behaviour.

It seems that neither of these capture what I'm trying to do here, which is really to guard against someone making a mathematical mistake in their coding.

That said, in your specific case and discarding shapeless's Nat option, no, the compiler doesn't support what you're trying to achieve. Please take a look at this.

Alejandro Echeverri
  • 1,328
  • 2
  • 19
  • 32