2009-06-13 14 views
56

Wiem, że newtype jest częściej porównywany z data w Haskell, ale przedstawiam to porównanie z bardziej projektowego punktu widzenia, niż jako techniczny problem.Typ Haskella kontra nowy w odniesieniu do bezpieczeństwa typu

W językach imperatywnych/OO występuje wzorzec "primitive obsession", w którym płodne stosowanie typów pierwotnych zmniejsza bezpieczeństwo typu programu i wprowadza przypadkową zamienność wartości tego samego typu, w innym przypadku przeznaczonych do różnych celów . Na przykład wiele rzeczy może być ciągiem, ale byłoby miło, gdyby kompilator mógł wiedzieć statycznie, co oznacza być imieniem i które mamy na myśli być miastem w adresie.

Jak często zatem programiści Haskell zatrudniają newtype, aby nadać rozróżnienie typu dla prymitywnych wartości? Zastosowanie type wprowadza alias i zapewnia czytelność semantyki programu, ale nie zapobiega przypadkowym zamianom wartości. Kiedy uczę się haskell, zauważam, że system typów jest tak potężny, jak każdy inny, z którym się zetknąłem. Dlatego uważam, że jest to naturalna i powszechna praktyka, ale nie widziałem wiele ani żadnej dyskusji na temat wykorzystania newtype w tym świetle.

Oczywiście wielu programistów robi różne rzeczy, ale czy to w ogóle powszechne w haskell?

+0

Hrm ... wygląda tak, że nie mogę oznaczyć więcej niż jednej odpowiedzi jako zaakceptowanej. Miałem nadzieję, że w jakiś sposób zaakceptuję rozsądną reprezentację różnych opinii na ten temat ... – StevenC

Odpowiedz

52

Główne zastosowania do newtypes są:

  1. Definiowanie alternatywne przykłady typów.
  2. Dokumentacja.
  3. Zapewnienie poprawności danych/formatów.

Pracuję teraz nad aplikacją, w której intensywnie używam newtypów. newtypes w Haskell są pojęciem wyłącznie kompilacji. Na przykład. z poniższymi archiwami, unFilename (Filename "x") skompilowany do tego samego kodu co "x". Nie ma absolutnie zero hit run-time. Dostępne są typy data. To sprawia, że ​​jest to bardzo dobry sposób na osiągnięcie wyżej wymienionych celów.

-- | A file name (not a file path). 
newtype Filename = Filename { unFilename :: String } 
    deriving (Show,Eq) 

Nie chcę przypadkowo traktować tego jako ścieżki do pliku. To nie jest ścieżka do pliku. Jest to nazwa pliku konceptualnego gdzieś w bazie danych.

Bardzo ważne jest, aby algorytmy odwoływały się do właściwych rzeczy, a nowe typy pomagają w tym. Jest to również bardzo ważne dla bezpieczeństwa, na przykład rozważ przesłanie plików do aplikacji internetowej. Mam te typy:

-- | A sanitized (safe) filename. 
newtype SanitizedFilename = 
    SanitizedFilename { unSafe :: String } deriving Show 

-- | Unique, sanitized filename. 
newtype UniqueFilename = 
    UniqueFilename { unUnique :: SanitizedFilename } deriving Show 

-- | An uploaded file. 
data File = File { 
    file_name  :: String   --^Uploaded file. 
    ,file_location :: UniqueFilename --^Saved location. 
    ,file_type  :: String   --^File type. 
    } deriving (Show) 

Załóżmy, że mam tę funkcję, która myje nazwę pliku z pliku, który został przekazany:

-- | Sanitize a filename for saving to upload directory. 
sanitizeFilename :: String   --^Arbitrary filename. 
       -> SanitizedFilename --^Sanitized filename. 
sanitizeFilename = SanitizedFilename . filter ok where 
    ok c = isDigit c || isLetter c || elem c "-_." 

Teraz od tego wygenerować unikalny Nazwa pliku:

-- | Generate a unique filename. 
uniqueFilename :: SanitizedFilename --^Sanitized filename. 
       -> IO UniqueFilename --^Unique filename. 

To niebezpieczne, aby wygenerować unikalną nazwę pliku z arbitralnej nazwy pliku, powinno być najpierw oczyszczone.Podobnie unikalna nazwa pliku jest zawsze bezpieczna przez rozszerzenie. Mogę teraz zapisać plik na dysk i umieścić go w mojej bazie danych, jeśli chcę.

Ale może być również denerwujące, aby dużo owijać/rozwierać. Na dłuższą metę uważam, że warto, szczególnie w celu uniknięcia niedopasowania wartości. ViewPatterns pomóc nieco:

-- | Get the form fields for a form. 
formFields :: ConferenceId -> Controller [Field] 
formFields (unConferenceId -> cid) = getFields where 
    ... code using cid .. 

Może powiesz, że rozpakowanie go w funkcji jest problemem - co zrobić, jeśli przechodzą cid do funkcji niewłaściwie? To nie problem, wszystkie funkcje używające identyfikatora konferencji będą używać typu ConferenceId. Wyłania się rodzaj systemu kontraktowego, który jest wymuszany podczas kompilacji. Nieźle. Tak więc używam go tak często, jak tylko mogę, szczególnie w dużych systemach.

+0

To niesamowicie fajne rzeczy Chris. Właśnie użyłem tego dla rozwiązania klasy typu do Real World Haskell rozdział 8 ćwiczenie 2 z pierwszego zestawu ćwiczeń. Prosi o podanie sposobu wybierania dopasowania globalnego nie uwzględniającego wielkości liter. Dziękuję :) –

+0

W jaki sposób ViewPattern w ostatnim przykładzie różni się od '(IDklienta cid)'? – Dan

+2

W moim przypadku nie eksportuję konstruktora, ponieważ nie chcę tworzyć dowolnych wartości z żadnej starej liczby całkowitej, powinien on pochodzić tylko z bazy danych. Mogę bezpiecznie odpakować i użyć tej liczby całkowitej. –

10

Myślę, że dość powszechne jest używanie newtype dla rozróżnień typów. W wielu przypadkach dzieje się tak dlatego, że chcesz podać różne instancje klasy lub ukryć implementacje, ale po prostu chęć ochrony przed przypadkowymi konwersjami jest oczywistym powodem, aby to zrobić.

19

Myślę, że to głównie kwestia sytuacji.

Zastanów się nazwy ścieżek. Standardowe prelude ma "type FilePath = String", ponieważ, dla wygody, chcesz mieć dostęp do wszystkich operacji na łańcuchach i listach. Gdybyś miał "newPpy FilePath = FilePath String", to potrzebowałbyś filePathLength, filePathMap i tak dalej, bo inaczej na zawsze używałbyś funkcji konwersji.

Z drugiej strony rozważ zapytania SQL. SQL injection jest częstym dziura bezpieczeństwa, więc warto mieć coś takiego

newtype Query = Query String 

a następnie dodać dodatkowe funkcje, które będzie przekonwertować ciąg w kwerendzie (lub fragmentu zapytań) uciekając znaki cudzysłowu lub wypełnić puste w szablonie w ten sam sposób. W ten sposób nie można przypadkowo przekonwertować parametru użytkownika na zapytanie bez przechodzenia przez funkcję wypisywania cudzysłowu.

+0

W odpowiedzi na przykład ścieżki pliku, pytanie jest bardziej w kontekście projektu, który robisz, a mniej o tym, co zostało już zaprojektowane gdzie nie masz kontroli. W poprzedniej sytuacji konsument twojego modułu/funkcji/cokolwiek nie zobaczy kodu, który uzyska prymityw. W tej ostatniej sytuacji najgorsze jest wezwanie do wycofania prymitywu tuż przed rozmową. Z drugiej strony, właśnie dlatego zadałem pytanie: dowiedzieć się, co różni programista programistów myśli o wyborze projektu. Wprawdzie jestem osobą, która skłania się ku bezpieczeństwu nad wygodą. – StevenC

+0

Tak jak powiedziałem, ponieważ chciałem poczuć różnicę. praktyki w kulturze haskell, twoja odpowiedź jest nadal cenna. Jeszcze nie skończyłem. :) – StevenC

+3

Rozumiem, że rozważasz własną praktykę projektową: chciałem tylko podać kilka praktycznych przykładów. Koszt mówienia "newtype FilePath" jest w czasie programisty; funkcje konwersji są po to, aby utrzymać sprawdzanie typu szczęśliwym i nie mieć implementacji. Najważniejsze jest to, że jeśli wielokrotnie konwertujesz i wylogowujesz się ze swojego nowego typu, nie masz żadnego dodatkowego bezpieczeństwa, po prostu dużo zaciemniania wywołań funkcji. Podczas projektowania biblioteki należy więc pomyśleć o punktach widzenia programistów aplikacji. –

14

W przypadku prostych deklaracji X = Y, dokumentacją jest type; newtype to sprawdzanie typu; dlatego newtype jest porównywany z data.

Dosyć często używam newtype do celów, które opisujesz: zapewniając, że coś, co jest przechowywane (i często manipulowane) w taki sam sposób, jak innego typu, nie jest mylone z czymś innym. W ten sposób działa jako nieznacznie bardziej wydajna deklaracja; nie ma żadnego szczególnego powodu, aby wybrać jeden nad drugim. Zauważ, że z rozszerzeniem GHC GeneralizedNewtypeDeriving, możesz automatycznie wyprowadzać klasy, takie jak Num, pozwalając na dodawanie i odejmowanie temperatur lub jena tak, jak to tylko możliwe, przy pomocy Int lub cokolwiek innego znajdującego się pod nimi. Ktoś jednak chce zachować ostrożność; zazwyczaj nie mnoży się temperatury przez inną temperaturę!

Na pomysł, jak często wykorzystywane są te rzeczy, W jednym racjonalnie dużego projektu pracuję nad teraz, mam około 122 zastosowań data, 39 zastosowań newtype i 96 zastosowań type.

Ale stosunek miarę „proste” typy są zainteresowane, jest nieco bliżej niż to pokazuje, bo 32 z tych 96 zastosowań type są rzeczywiście aliasy dla typów funkcyjnych, takich jak

type PlotDataGen t = PlotSeries t -> [String] 

Zauważysz tutaj dwie dodatkowe komplikacje: po pierwsze, jest to właściwie typ funkcji, a nie tylko prosty alias X = Y, a po drugie, że jest sparametryzowany: PlotDataGen jest konstruktorem typu stosowanym do innego typu w celu utworzenia nowego typu, takiego jak PlotDataGen (Int,Double) . Kiedy zaczynasz robić tego rodzaju rzeczy, type nie jest już tylko dokumentacją, ale w rzeczywistości jest funkcją, ale na poziomie typu, a nie na poziomie danych.

newtype jest czasami stosowany tam, gdzie nie może być type, na przykład, gdy konieczne jest zdefiniowanie typu rekursywnego, ale uważam, że jest to dość rzadkie. Wygląda więc na to, że przynajmniej w tym konkretnym projekcie około 40% moich "pierwotnych" definicji typów to newtype s, a 60% to type. Niektóre z definicji newtype były typami i zostały zdecydowanie przekonwertowane z dokładnie wymienionych powodów.

Krótko mówiąc, jest to częsty idiom.

Powiązane problemy