在编译时强制执行scala varargs的非空洞

时间:2016-10-17 20:06:12

标签: scala

我有一个函数需要相同类型的可变数量的参数,这听起来像varargs的教科书用例:

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

我遇到的问题是myFunc无法接受空参数列表。在运行时有一种简单的方法来强制执行:

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

问题在于它发生在运行时,而不是编译时。我希望编译器拒绝myFunc()

一种可能的解决方案是:

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

当使用内联参数调用myFunc时,这是有效的,但我希望我的库的用户能够传入List[A],这种语法非常笨拙。

我可以尝试两者兼得:

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

但是我们回到了我们开始的地方:现在有一种方法可以使用空参数列表调用myFunc

我知道scalaz的NonEmptyList,但在尽可能多的情况下,我想继续使用stlib类型。

有没有办法通过标准库来实现我的想法,或者我是否需要接受一些运行时错误处理,以获得真正感觉编译器应该能够处理的内容?

3 个答案:

答案 0 :(得分:5)

这样的事情怎么样?

implicitNotFound

使用具有适当{{1}}注释的类替换Nothing应该允许显示错误消息。

答案 1 :(得分:3)

让我们从我认为是您的基本要求开始:以某种方式定义myFunc的能力,以便在用户提供文字时在Scala控制台上发生以下情况。那么也许如果我们能够做到这一点,我们可以尝试去争取。

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

此外,我们不希望强制用户将列表拆分为头尾,或将其转换为::

当我们给出文字时,由于我们可以访问用于构造值的语法,我们可以使用宏来验证列表是否为空。此外,已经有一个图书馆会为我们做这件事,即refined

scala> refineMV[NonEmpty]("Hello")
res2: String Refined NonEmpty = Hello

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

不幸的是,在您的情况下,这仍然存在问题,因为您需要将refineMV放入函数体中,此时文字语法消失且宏魔法失败。

那么不依赖于语法的一般情况呢?

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

现在我们靠墙了;因为代码已经被编译而且xs显然是空的,所以在这里不会发生编译时错误。我们必须在运行时处理这种情况,要么是以Option之类的类型安全方式,要么是运行时异常之类的东西。但也许我们可以做得更好,而不仅仅是举起手来。有两种可能的改进途径。

  1. 以某种方式提供implicit xs非空的证据。如果编译器能找到那个证据,那就好了!如果没有,用户可以在运行时以某种方式提供它。
  2. 通过您的计划跟踪xs的出处并静态证明它必须是非空的。如果无法证明这一点,则在编译时出错或以某种方式强制用户处理空案例。
  3. 不幸的是,这很有问题。

    1. 我强烈怀疑这是不可能的(但这只是一种怀疑,我很乐意被证明是错的)。原因是最终implicit解析是类型导向的,这意味着Scala能够对类型进行类型级计算,但Scala没有我知道的对值进行类型级计算的机制(即依赖打字)。我们在此要求使用后者,因为List(1, 2, 3)List[Int]()在类型级别上无法区分。
    2. 现在你已经进入了SMT解决方案领域,它在其他语言方面做了一些努力(你好Liquid Haskell!)。可悲的是,我不知道Scala中的任何此类努力(我想在Scala中这将是一项艰巨的任务)。
    3. 最重要的是,在进行错误检查时,没有免费的午餐。编译器不能神奇地使错误处理消失(虽然它可以告诉你什么时候你并不严格需要它),当你忘记处理某些类错误时,它能做的最好就是对你大喊大叫,这本身就很有价值。为了强调没有免费午餐点,让我们回到具有依赖类型(Idris)的语言,看看它如何处理List的非空值以及在空列表中打破的原型函数, List.head

      首先我们在空列表上得到编译错误

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

      好,非空列表怎么样,即使它们被一些跳跃模糊了?

      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
      

      它有点工作!如果我们真的不让Idris编译器知道我们传递的数字,而是在运行时从用户那里得到一个怎么办?我们发现了一个以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
      

      嗯...那么List.head的类型签名是什么?

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

      啊我们只需提供一个NonEmpty

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

      ::。而我们又回到了第一个方向。

答案 2 :(得分:2)

使用scala.collection.immutable.::

::是列表的缺点

在std lib中定义

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

使用::来定义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