Running non-empty scala varargs at compile time

I have a function that expects a variable number of parameters of the same type, which sounds like an example of using a tutorial for varargs:

def myFunc[A](as: A*) = ??? 

The problem is that myFunc cannot accept empty parameter lists. There is a trivial way to ensure this at runtime:

 def myFunc[A](as: A*) = { require(as.nonEmpty) ??? } 

The problem is that this happens at run time, as opposed to compile time. I would like the compiler to reject myFunc() .

One possible solution:

 def myFunc[A](head: A, tail: A*) = ??? 

And this works when myFunc is called with built-in arguments, but I want the users of my library to be able to go to List[A] , which makes this syntax very inconvenient.

I could try both:

 def myFunc[A](head: A, tail: A*) = myFunc(head +: tail) def myFunc[A](as: A*) = ??? 

But we returned to where we started: now there is a way to call myFunc with an empty list of parameters.

I know about scalaz NonEmptyList , but as much as possible I would like to stop with strobile types.

Is there a way to achieve what I mean only with the standard library, or do I need to take some error handling at runtime for something that really looks like a compiler to deal with?

+5
source share
3 answers

How about this?

 scala> :paste // Entering paste mode (ctrl-D to finish) def myFunc()(implicit ev: Nothing) = ??? def myFunc[A](as: A*) = println(as) // Exiting paste mode, now interpreting. myFunc: ()(implicit ev: Nothing)Nothing <and> [A](as: A*)Unit myFunc: ()(implicit ev: Nothing)Nothing <and> [A](as: A*)Unit scala> myFunc(3) WrappedArray(3) scala> myFunc(List(3): _*) List(3) scala> myFunc() <console>:13: error: could not find implicit value for parameter ev: Nothing myFunc() ^ scala> 

Replacing Nothing with a class that has the corresponding annotation implicitNotFound should contain a reasonable error message.

+5
source

Let's start with what you think is your basic requirement: the ability to define myFunc some way so that the following is done on the Scala console: the user provides literals. Then, perhaps if we can achieve this, we can try switching to varargs.

 myFunc(List(1)) // no problem myFunc(List[Int]()) // compile error! 

In addition, we do not want to force users to split the list into head and tail or convert them to :: .

Well, when we are given literals, since we have access to the syntax used to construct the value, we can use macros to verify that the list is not empty. In addition, there is already a library that will do this for us, namely refined !

 scala> refineMV[NonEmpty]("Hello") res2: String Refined NonEmpty = Hello scala> refineMV[NonEmpty]("") <console>:39: error: Predicate isEmpty() did not fail. refineMV[NonEmpty]("") ^ 

Unfortunately, this is still problematic in your case, because you need to put refineMV in the body of your function, after which the literal disappears syntactically and the macromagic doesn't work.

OK, what about a general case that is syntax independent?

 // Can we do this? val xs = getListOfIntsFromStdin() // Pretend this function exists myFunc(xs) // compile error if xs is empty 

OK, now we are against the wall; there cannot be a compile-time error, since the code has already been compiled, but it is clear that xs may be empty. We will have to deal with this case at runtime, either with a type, or with Option and the like, or with something like runtime exceptions. But perhaps we can do a little better than just raise our hands up. There are two possible ways to improve.

  • Somehow provide implicit evidence that xs is not empty. If the compiler can find this evidence, then great! Otherwise, the user must provide it somehow at runtime.
  • Track the origin of xs through your program and statically prove that it must be non-empty. If this cannot be proven, either a compile-time error or somehow forces the user to handle the empty case.

Once again, unfortunately, this is problematic.

  • I strongly suspect that this is impossible (but this is still just a suspicion, and I would be happy if I were wrong). The reason is that, in the end, the implicit resolution has type-directivity, which means that Scala gets the ability to perform type-level calculations by type, but Scala does not have a mechanism that, as I know, performs level-level calculations by values ​​( i.e. dependent typing). Here we require the latter, because List(1, 2, 3) and List[Int]() are indistinguishable at the type level.
  • You are now on an SMT-decisive land that has some efforts in other languages ​​(hello Liquid Haskell !). Unfortunately, I do not know any such efforts in Scala (and I believe that in Scala it would be more difficult to complete the task).

The bottom line is that when checking for errors there is no free lunch . The compiler cannot magically force error handling to go away (although it can tell you when you don't need it), the best thing it can do is yell at you when you forget to process certain classes of errors, which in itself is very valuable. To emphasize the lack of a free lunch break, return a language that has dependent types (Idris) and see how it handles non-empty List values ​​and a prototype function that breaks into empty lists, List.head .

First we get a compilation error in empty lists

 Idris> List.head [] (input):1:11:When checking argument ok to function Prelude.List.head: Can't find a value of type NonEmpty [] 

Well, what about non-empty lists, even if they are confused by a couple of jumps?

 Idris> :let x = 5 -- Below is equivalent to -- val y = identity(Some(x).getOrElse(3)) Idris> :let y = maybe 3 id (Just x) -- Idris makes a distinction between Natural numbers and Integers -- Disregarding the Integer to Nat conversion, this is -- val z = Stream.continually(2).take(y) Idris> :let z = Stream.take (fromIntegerNat y) (Stream.repeat 2) Idris> List.head z 2 : Integer 

It works somehow! What if we really do not allow the Idris compiler to know anything about the amount that we pass and instead get it at runtime from the user? We explode with a really gigantic error message that starts with When checking argument ok to function Prelude.List.head: Can't find a value of type NonEmpty...

 import Data.String generateN1s : Nat -> List Int generateN1s x = Stream.take x (Stream.repeat 1) parseOr0 : String -> Nat parseOr0 str = case parseInteger str of Nothing => 0 Just x => fromIntegerNat x z : IO Int z = do x <- getLine let someNum = parseOr0 x let firstElem = List.head $ generateN1s someNum -- Compile error here pure firstElem 

Hmmm ... well, what is a signature like List.head ?

 Idris> :t List.head -- {auto ...} is roughly the same as Scala implicit head : (l : List a) -> {auto ok : NonEmpty l} -> a 

Ah, so we just need to provide a NonEmpty .

 data NonEmpty : (xs : List a) -> Type where IsNonEmpty : NonEmpty (x :: xs) 

Oh a :: . And we returned to the square.

+3
source

Use scala.collection.immutable.::

:: is a minus list

defined in std lib

 ::[A](head: A, tail: List[A]) 

use :: to define myFunc

 def myFunc[A](list: ::[A]): Int = 1 def myFunc[A](head: A, tail: A*): Int = myFunc(::(head, tail.toList)) 

Scala repl

 scala> def myFunc[A](list: ::[A]): Int = 1 myFunc: [A](list: scala.collection.immutable.::[A])Int scala> def myFunc[A](head: A, tail: A*): Int = myFunc(::(head, tail.toList)) myFunc: [A](head: A, tail: A*)Int 
+2
source

Source: https://habr.com/ru/post/1258363/


All Articles