Zacznijmy od tego, co moim zdaniem jest podstawowym wymogiem: możliwość zdefiniowania myFunc
w pewien sposób taki, że następujące czynności występują w konsoli Scala, gdy użytkownik podaje literały. Wtedy może, jeśli uda nam się to osiągnąć, możemy spróbować przejść na varargs.
myFunc(List(1)) // no problem
myFunc(List[Int]()) // compile error!
Co więcej, nie chcemy, aby zmusić użytkowników albo podzielić listę na głowie i ogonie lub je przekonwertować do ::
.
Cóż, gdy otrzymujemy literały, ponieważ mamy dostęp do składni użytej do skonstruowania wartości, możemy użyć makr, aby sprawdzić, czy lista nie jest pusta. Co więcej, istnieje już biblioteka, która zrobi to za nas, a mianowicie refined!
scala> refineMV[NonEmpty]("Hello")
res2: String Refined NonEmpty = Hello
scala> refineMV[NonEmpty]("")
<console>:39: error: Predicate isEmpty() did not fail.
refineMV[NonEmpty]("")
^
Niestety to wciąż problematyczne w przypadku, bo trzeba umieścić refineMV
w ciało swojej funkcji, w której punkt dosłowne składniowo znika i makro magia zawodzi.
Okej, co z ogólnym przypadkiem, który nie polega na składni?
// Can we do this?
val xs = getListOfIntsFromStdin() // Pretend this function exists
myFunc(xs) // compile error if xs is empty
Teraz stoimy pod ścianą; nie ma mowy, aby błąd kompilacji mógł się tu wydarzyć, ponieważ kod został już skompilowany, a jednak wyraźnie może być pusty. Będziemy musieli poradzić sobie z tą sprawą w czasie wykonywania, albo w sposób bezpieczny dla typów z Option
i tym podobnymi, albo z czymś podobnym do wyjątków w czasie wykonywania. Ale może możemy zrobić trochę lepiej, niż po prostu wyrzucić ręce w powietrze. Istnieją dwie możliwe drogi poprawy.
- Jakoś dostarczyć
implicit
dowód, że xs
jest niepusty. Jeśli kompilator może znaleźć te dowody, to świetnie! Jeśli nie, to użytkownik musi to jakoś udostępnić w czasie wykonywania.
- Śledź pochodzenie
xs
za pośrednictwem programu i statycznie udowodnić, że nie jest pusty. Jeśli nie można tego udowodnić, albo wystąpi błąd podczas kompilacji, albo w jakiś sposób zmusi użytkownika do obsłużenia pustego przypadku.
Po raz kolejny niestety jest to problematyczne.
- Podejrzewam, że nie jest to możliwe (ale jest to nadal tylko podejrzenie i byłbym szczęśliwy, gdyby okazało się, że się nie mylę). Powodem jest to, że ostatecznie rozdzielczość
implicit
jest kierowana przez typ, co oznacza, że Scala ma możliwość wykonywania obliczeń na poziomie typu na typach, ale Scala nie ma mechanizmu, o którym wiem, że wykonałby obliczenia na poziomie typu na wartościach (tj. Zależne pisanie). Wymagamy tego ostatniego, ponieważ List(1, 2, 3)
i List[Int]()
są nieodróżnialne na poziomie typu.
- Teraz jesteś w strefie rozwiązywania problemów SMT, która ma trochę wysiłku w innych językach (witam Liquid Haskell!). Niestety nie znam żadnych takich działań w Scali (i wyobrażam sobie, że byłoby to trudniejsze zadanie w Scali).
Najważniejsze jest to, że jeśli chodzi o sprawdzanie błędów nie ma darmowych obiadów. Kompilator nie może w magiczny sposób przestać obsługiwać błędów (chociaż może ci powiedzieć, kiedy nie jest to bezwzględnie potrzebne), najlepsze, co może zrobić, to krzyczeć na ciebie, gdy zapomnisz poradzić sobie z pewnymi klasami błędów, który sam w sobie jest bardzo cenny. Aby podkreślić nie wolny punkt obiadowy, wróćmy do języka, który ma typy zależne (Idris) i zobaczmy, jak radzi sobie z niepustymi wartościami List
i prototypową funkcją, która łamie się na pustych listach, List.head
.
Najpierw pojawia się błąd kompilacji na pustych list
Idris> List.head []
(input):1:11:When checking argument ok to function Prelude.List.head:
Can't find a value of type
NonEmpty []
dobry, co nie pustych list, nawet jeśli są one zaciemniony przez parę skoków?
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
To jakoś działa! A co, jeśli naprawdę nie pozwolimy, aby kompilator Idris wiedział cokolwiek o numerze, który przekazujemy, a zamiast tego dostaniemy jeden w czasie wykonywania od użytkownika? Mamy wysadzić z naprawdę gigantycznego komunikat o błędzie, który zaczyna się 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 ... dobrze, co jest podpis typu List.head
?
Idris> :t List.head
-- {auto ...} is roughly the same as Scala's implicit
head : (l : List a) -> {auto ok : NonEmpty l} -> a
Ah, więc musimy po prostu dostarczyć NonEmpty
.
data NonEmpty : (xs : List a) -> Type where
IsNonEmpty : NonEmpty (x :: xs)
Oh a ::
. I wróciliśmy do pierwszego kwadratu.
A jeśli twój użytkownik prześle ci pustą listę? Naprawdę uważam, że "def myFunc [A] (head: A, tail: A *) = ..." jest drogą do zrobienia. Bardzo prosto przekazuje użytkownikowi intencję, że musisz podać mu co najmniej jeden argument. – badcook
@badcook Zgadzam się, że jest to standard, ale jest to również uciążliwe, jeśli chcesz przekazać Seq do innej funkcji, ponieważ musisz ją odbudować (prześlij od głowy do końca). Zastanawiam się, dlaczego nie jest to większa oferta. Mogą mieć składnię 'def f (x: X +)'. Myślę, że dawno temu powstał pomysł na składnię podobną do regex. –
@ som-snytt tego rodzaju składnia wydaje się trudna do wykonania w czasie kompilacji bez typu "NonEmptyList", którego "_ *" może odłączyć (chyba że po prostu upuścisz '_ *'). Najprawdopodobniej będziesz musiał zrobić to samo, odbudować ogony, aby uzyskać "NonEmptyList" w pierwszej kolejności. – badcook