我有一个函数需要相同类型的可变数量的参数,这听起来像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类型。
有没有办法通过标准库来实现我的想法,或者我是否需要接受一些运行时错误处理,以获得真正感觉编译器应该能够处理的内容?
答案 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
之类的类型安全方式,要么是运行时异常之类的东西。但也许我们可以做得更好,而不仅仅是举起手来。有两种可能的改进途径。
implicit
xs
非空的证据。如果编译器能找到那个证据,那就好了!如果没有,用户可以在运行时以某种方式提供它。xs
的出处并静态证明它必须是非空的。如果无法证明这一点,则在编译时出错或以某种方式强制用户处理空案例。不幸的是,这很有问题。
implicit
解析是类型导向的,这意味着Scala能够对类型进行类型级计算,但Scala没有我知道的对值进行类型级计算的机制(即依赖打字)。我们在此要求使用后者,因为List(1, 2, 3)
和List[Int]()
在类型级别上无法区分。 最重要的是,在进行错误检查时,没有免费的午餐。编译器不能神奇地使错误处理消失(虽然它可以告诉你什么时候你并不严格需要它),当你忘记处理某些类错误时,它能做的最好就是对你大喊大叫,这本身就很有价值。为了强调没有免费午餐点,让我们回到具有依赖类型(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