Scala SNAFUs

tags: scala

I am not a huge fan of Scala. Most languages I have used I get used to eventually, and then eventually get to like. Not all of them equally and not for all tasks, but I usually see some use to them. Scala is a different story. I have been using it since I started my PhD and I still have not gotten used to it. I am still not proficient with it, and it still feels jarring to me.

This is a list of minor to major grievances I have with it. They are not meant to be constructive (though some of them could be), as much as left here for someone else who feels the same to identify with. I don't think I am the target audience of Scala, and that's fine. I don't have nor do I want to have any power over Scala.

  1. requiring sbt reload is begging for trouble from using old configs
  2. no newtype
  3. working on intertwined packages locally requires impractical +publishLocal workflows that beg for someone to forget to do the publishing and then debugging old code for a long time
  4. implicit conversions that go wrong make for terrible compiler messages
  5. early init of traits cause unexpected values, crashes on empty lists and compiler (or worse, runtime) errors are completely incomprehensible when this happens
  6. == defined for different types (that always compare unequal), causing unexpected behaviour, in particular if some of those values are transparently wrapped by something
  7. assembly produces broken JARs on case-insensitive file systems, sometimes, without clear warnings (for me, sbt assembly always outputs warnings so it's taught me to ignore them)
  8. path-dependent types cannot be referred to anywhere and thus are more or less useless
  9. it’s possible to use a for loop over a monadic Option, and I do not like it:
    val hasValue = Option("hello")
    for (v <- hasValue) {
      // will run 0 or 1 times but it looks like a normal loop
    }
    
  10. Please tell me what this function does
    private def checkLengthConsistency : Option[Seq[TermConstraint]] = {
        Console.err.println("checkLengthConsistency")
        for (p <- lengthProver;
             if {
               Console.err.println(p.???)
               if (debug)
                 Console.err.println("checking length consistency")
               measure("check length consistency") {p.???}
               assert(p.??? == ProverStatus.Unsat || p.??? == ProverStatus.Sat,
                      s"${p.???} not SAT or UNSAT")
               p.??? == ProverStatus.Unsat
             }) yield {
          Console.err.println(p.certificateAsString(Map(), ap.parameters.Param.InputFormat.SMTLIB))
          for (n <- p.getUnsatCore.toList.sorted;
               if n > 0;
               c <- lengthPartitions(n - 1))
          yield c
        }
      }
    
  11. orElse for lists is not what you would expect (or want) and does not use the list monad
  12. No way to define aliases for long types at root level
  13. Long types often required explicitly because type elision is too weak
  14. Spent literally hours failing to get a debugger to work
  15. Running command line programs with arguments through SBT is a horrid mess
  16. Test results in SBT are unreliable when more than one test is run, failures can show up for the wrong test. This is the default setting.
  17. Using mixins for cross-cutting concerns does not mix well with flow-style programming. Consider this logging mixin:
    trait Logging {
      protected var loggingEnabled = false
      def enableLogging() = {
        loggingEnabled = true
        this
      } // Returns something of type Tracing.
    }
    
    class MyWidget extends Logging {
      def doWork(): Unit = {}
    }
    
    val widget = new MyWidget().enableLogging()
    widget.doWork() /* Compile error unless we 
                       cast foo to MyWidget because foo is a Tracing */
                       ```
    
  18. first flatMap second catches any exception that happens in second, regardless of whether it’s wrapped in a Try or not. However, if first triggers an exception outside of a Try block the exception is thrown as expected. This makes it hard to promote exceptions up the call stack:
    import scala.util.Try
    
    def firstStepSucceeds(): Try[Int] = Try {
      1
    }
    
    def firstStepFails(): Try[Int] = Try {
      throw new Exception("First step failed")
      1
    }
    
    def secondStepSucceeds(input: Int) = Try {
      input + 1
    }
    
    def secondStepFails(input: Int) = Try {
      throw new Exception("Second step failed in try block!")
      input + 1
    }
    
    def secondStepThrows(input: Int) = {
      throw new Exception("Exception thrown in second step")
    }
    
    // Works as expected
    firstStepSucceeds() flatMap (secondStepSucceeds _) 
    // Success(2)
    
    firstStepFails() flatMap (secondStepSucceeds _) 
    // Failure: First step failed
    firstStepSucceeds() flatMap (secondStepFails _)
    // Failure: Second step failed in try block
    
    // I expect this to throw Exception in second step, but:
    firstStepSucceeds() flatMap (secondStepThrows _)
    // ... I get Failure(Exception thrown in second step),
    // e.g. the exception is automatically caught
    
  19. Case classes interacts horribly with subtyping, requiring type annotations:
      def runWithTimeout[T](p: SimpleAPI)(block: => T): Either[Timeout, T] =
        timeout_ms match {
          case Some(timeout_ms) =>
            try {
              p.withTimeout(timeout_ms) {
                ap.util.Timeout.withTimeoutMillis(timeout_ms) {
                  Right(block): Either[Timeout, T]
                } {
                  Left(Timeout(timeout_ms)): Either[Timeout, T]
                }
              }
            } catch {
              case SimpleAPI.TimeoutException | ap.util.Timeout(_) =>
                Left(Timeout(timeout_ms)): Either[Timeout, T]
            }
          case None => Right(block)
        }
    
  20. Capturing and propagating failures in case classes does not work as expected due to subtyping:
    somethingThatCanError() match {
    	case Success(successValue) => someOtherOperation(successValue)
    	case f: Failure[_] => f // Triggers a type error because f is the wrong type of Try, however this works:
    	case Failure(f) => Failure(f)
    }
    
  21. You cannot do an early return from a loop except using exceptions, and that doesn't always work like you want it to.
  22. Compiler linting is so arbitrary you more or less have to turn it off.
  23. The build system spews temporary files everywhere and is very difficult to get to behave predictably in the loose sense of consistently either failing or succeeding. It also randomly downloads stuff from the internet, and does not seem to be able to work offline. I cannot imagine what it would take to get deterministic builds.