2016-10-17 17 views
5

Mam funkcję, która oczekuje zmienną liczbę parametrów tego samego typu, co brzmi jak przypadku użycia podręcznik dla varargs:Egzekwowanie non-Emptyness Scala varargs w czasie kompilacji

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

Mam problem jest to, że myFunc nie może przyjmować pustych list parametrów. Jest trywialny sposób egzekwowania, że ​​w czasie wykonywania:

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

Problem z tym jest to, że dzieje się wykonywania, w przeciwieństwie do czasu kompilacji. Chciałbym, aby kompilator odrzucił myFunc().

Możliwym rozwiązaniem byłoby:

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

I to działa, gdy myFunc nazywamy argumentami inline, ale chciałbym użytkownikom mojej bibliotece, aby móc przekazać w List[A], co ta składnia marki bardzo dziwne.

mogę starać się mieć zarówno:

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

Ale jesteśmy z powrotem do punktu wyjścia: istnieje obecnie sposób wywołania myFunc z pustą listą parametrów.

Jestem świadomy skalazu NonEmptyList, ale w miarę możliwości, chciałbym pozostać z typami typu.

Czy istnieje sposób, aby osiągnąć to, co mam na myśli przy użyciu standardowej biblioteki, czy też muszę zaakceptować obsługę błędów w czasie wykonywania dla czegoś, co naprawdę wydaje się, że kompilator powinien być w stanie sobie poradzić?

+1

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

+0

@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. –

+0

@ 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

Odpowiedz

2

Zastosowanie scala.collection.immutable.::

:: jest wad listy

zdefiniowane STD lib

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

stosowania :: zdefiniować 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 
+0

Sprytny, ale czuję, że tak naprawdę nie rozwiązuje problemu: jeśli mam 'List [A]' do przekazania do 'myFunc', nadal muszę przesłać go do' :: ', które jest sprawdzane w czasie wykonywania . –

+0

@NicolasRinaudo Użytkownicy muszą wysłać ': :(1, List (2))' do myFunc. Nie możemy wziąć Listy jako danych wejściowych, ponieważ Lista może być pusta. To dlatego przyjmujemy 'cons' jako dane wejściowe. – pamu

+2

@NicolasRinaudo Nie widzę innego sposobu, aby to zrobić. Nie można zezwolić użytkownikom na wysyłanie listy jako danych wejściowych i wymuszanie braku pustki na liście podczas kompilacji. Dobrze, że nie musisz korzystać z zewnętrznych bibliotek. – pamu

5

Co o czymś takim?

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> 

Wymiana Nic z klasą, który ma odpowiednią implicitNotFound adnotacji powinno pozwolić na sensowny komunikat o błędzie.

+0

To jest dobre. I upewnij się, że klasa ev jest zapieczętowana. –

+0

To zdecydowanie fajna sztuczka! –

+2

@YuvalItzchakov ale nadal możesz użyć go w ten sposób 'myFunc (List(): _ *)' .. który OP nie lubi/chce – pamu

3

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.

  1. 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.
  2. Ś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.

  1. 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.
  2. 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.