Dlaczego nie opisać parametrów funkcji?

28

Aby odpowiedzieć na to pytanie, załóżmy, że koszt dwuznaczności w umyśle programisty jest znacznie droższy niż kilka dodatkowych naciśnięć klawiszy.

Biorąc to pod uwagę, dlaczego miałbym pozwalać moim kolegom z zespołu na ucieczkę bez adnotowania ich parametrów funkcji? Weź następujący kod jako przykład czegoś, co może być o wiele bardziej złożonym fragmentem kodu:

let foo x y = x + y

Teraz, szybkie sprawdzenie podpowiedzi pokaże, że F # ustaliło, że chciałeś, aby xiy były ints. Jeśli tak zamierzałeś, wszystko jest w porządku. Ale ja nie wiem, czy to, co zamierzałeś. Co jeśli utworzyłeś ten kod, aby połączyć dwa ciągi razem? A co jeśli myślę, że prawdopodobnie chciałeś dodać podwójne? A jeśli nie chcę po prostu najeżdżać myszką na każdy parametr funkcji, aby określić jej typ?

Teraz weź to jako przykład:

let foo x y = "result: " + x + y

F # zakłada teraz, że prawdopodobnie zamierzałeś łączyć łańcuchy, więc xiy są zdefiniowane jako łańcuchy. Jednak jako biedny schmuck, który utrzymuje Twój kod, mogę na to spojrzeć i zastanawiać się, czy być może nie chciałeś dodać x i y (int) razem, a następnie dołączyć wynik do ciągu dla celów interfejsu użytkownika.

Z pewnością w przypadku takich prostych przykładów można to odpuścić, ale dlaczego nie narzucić polityki wyraźnych adnotacji typu?

let foo (x:string) (y:string) = "result: " + x + y

Jaka jest szkoda w jednoznaczności? Pewnie, programista może wybrać niewłaściwe typy do tego, co próbują zrobić, ale przynajmniej wiem, że zamierzali to zrobić, że to nie był tylko przeoczenie.

To poważne pytanie ... Nadal jestem nowy na F # i wytyczam szlak dla mojej firmy. Standardy, które przyjmę, będą prawdopodobnie podstawą wszystkich przyszłych kodowań F #, osadzonych w nieskończonym kopiowaniu, które z pewnością przenikną do kultury przez wiele lat.

Więc ... czy jest coś specjalnego w wnioskowaniu typu F #, co czyni z niego cenną funkcję do trzymania, adnotację tylko w razie potrzeby? A może eksperci F # mają zwyczaj dodawania adnotacji do swoich parametrów w nie trywialnych zastosowaniach?

JDB
źródło
Wersja z adnotacjami jest mniej ogólna. Z tego samego powodu najlepiej jest używać IEnumerable <> w podpisach, a nie SortedSet <>.
Patrick,
2
@Patrick - Nie, nie jest, ponieważ F # wnioskuje o typie ciągu. Nie zmieniłem podpisu funkcji, tylko wyraźnie ją wyraziłem.
JDB,
1
@Patrick - A nawet jeśli to prawda, czy możesz wyjaśnić, dlaczego jest to zła rzecz? Być może podpis powinien być mniej ogólny, jeśli tak zamierzał programista. Być może bardziej ogólna sygnatura faktycznie powoduje problemy, ale nie jestem pewna, jak konkretny zamierzał być programista bez dużej ilości badań.
JDB,
1
Myślę, że można uczciwie powiedzieć, że w językach funkcjonalnych wolisz ogólność i adnotacje w bardziej konkretnym przypadku; np. gdy chcesz uzyskać lepszą wydajność i możesz ją uzyskać za pomocą podpowiedzi typu. Używanie funkcji z adnotacjami wszędzie wymagałoby pisania przeciążeń dla każdego określonego typu, co może nie być uzasadnione w ogólnym przypadku.
Robert Harvey,

Odpowiedzi:

30

Nie używam F #, ale w Haskell uważa się za dobrą formę adnotacji (przynajmniej) definicji najwyższego poziomu, a czasem definicji lokalnych, nawet jeśli język ma wszechobecne wnioskowanie o typach. Jest to z kilku powodów:

  • Czytanie
    Jeśli chcesz wiedzieć, jak korzystać z funkcji, niezwykle przydatna jest dostępność podpisu typu. Możesz to po prostu przeczytać, zamiast próbować wnioskować o tym samemu lub polegać na narzędziach, które zrobią to za Ciebie.

  • Refaktoryzacja
    Jeśli chcesz zmienić funkcję, posiadanie wyraźnego podpisu daje pewną pewność, że Twoje transformacje zachowają intencję oryginalnego kodu. W języku opartym na typach może się okazać, że wysoce polimorficzny kod sprawdzi typ, ale nie zrobi tego, co zamierzałeś. Podpis typu jest „barierą”, która konkretyzuje informacje o typie w interfejsie.

  • Wydajność
    W Haskell wywnioskowany typ funkcji może być przeciążony (za pomocą klas), co może oznaczać wysłanie w czasie wykonywania. W przypadku typów numerycznych domyślnym typem jest liczba całkowita o dowolnej dokładności. Jeśli nie potrzebujesz pełnej ogólności tych funkcji, możesz poprawić wydajność, specjalizując funkcję dla określonego typu, którego potrzebujesz.

W przypadku definicji lokalnych, letzmiennych związanych i parametrów formalnych do lambd, uważam, że podpisy typu zwykle kosztują więcej w kodzie niż wartość, którą dodaliby . Dlatego podczas przeglądu kodu sugerowałbym, aby nalegać na podpisy dla definicji najwyższego poziomu i po prostu poprosić o rozsądne adnotacje w innym miejscu.

Jon Purdy
źródło
3
To brzmi jak bardzo rozsądna i dobrze wyważona reakcja.
JDB,
1
Zgadzam się, że posiadanie definicji z wyraźnie zdefiniowanymi typami może pomóc przy refaktoryzacji, ale uważam, że testowanie jednostkowe może również rozwiązać ten problem i ponieważ uważam, że test jednostkowy powinien być używany z programowaniem funkcjonalnym, aby upewnić się, że funkcja działa zgodnie z przeznaczeniem przed użyciem z inną funkcją, pozwala mi to pominąć typy z definicji i mieć pewność, że jeśli dokonam przełomowej zmiany, zostanie ona złapana. Przykład
Guy Coder,
4
W Haskell poprawne jest dodawanie adnotacji do twoich funkcji, ale uważam, że idiomatyczne F # i OCaml zwykle pomijają adnotacje, chyba że jest to konieczne do usunięcia niejednoznaczności. Nie oznacza to, że jest szkodliwy (choć składnia do adnotacji w F # jest brzydsza niż w Haskell).
KChaloux,
4
@KChaloux W OCaml istnieją już adnotacje w .mlipliku interface ( ) (jeśli piszesz takie, do których jesteś bardzo zachęcany. Adnotacje typu są zwykle pomijane w definicjach, ponieważ byłyby zbędne z interfejsem.
Gilles ' SO- przestań być zły
1
@Gilles @KChalhal rzeczywiście, to samo w .fsiplikach F # Signature ( ), z jednym zastrzeżeniem: F # nie używa plików podpisu do wnioskowania o typie, więc jeśli coś w implementacji jest niejednoznaczne, nadal będziesz musiał to zrobić. books.google.ca/…
Yawar Amin
1

Jon udzielił rozsądnej odpowiedzi, której nie powtórzę tutaj. Pokażę ci jednak opcję, która może zaspokoić twoje potrzeby, a podczas tego procesu zobaczysz inny rodzaj odpowiedzi niż tak / nie.

Ostatnio pracuję nad analizowaniem składni za pomocą kombinatorów parsera. Jeśli znasz parsowanie, to wiesz, że zazwyczaj używasz leksykera w pierwszej fazie i parsera w drugiej fazie. Lexer konwertuje tekst na tokeny, a parser konwertuje tokeny na AST. Teraz, gdy F # jest językiem funkcjonalnym, a kombinatory są zaprojektowane do łączenia, kombinatory parsera są zaprojektowane do korzystania z tych samych funkcji zarówno w leksyrze, jak i parserze, ale jeśli ustawisz typ funkcji kombinatora parsera, możesz ich użyć tylko do leksykon lub parsowanie, a nie jedno i drugie.

Na przykład:

/// Parser that requires a specific item.

// a (tok : 'a) : ('a list -> 'a * 'a list)                     // generic
// a (tok : string) : (string list -> string * string list)     // string
// a (tok : token)  : (token list  -> token  * token list)      // token

lub

/// Parses iterated left-associated binary operator.


// leftbin (prs : 'a -> 'b * 'c) (sep : 'c -> 'd * 'a) (cons : 'd -> 'b -> 'b -> 'b) (err : string) : ('a -> 'b * 'c)                                                                                    // generic
// leftbin (prs : string list -> string * string list) (sep : string list -> string * string list) (cons : string -> string -> string -> string) (err : string) : (string list -> string * string list)  // string
// leftbin (prs : token list  -> token  * token list)  (sep : token list  -> token  * token list)  (cons : token  -> token  -> token  -> token)  (err : string) : (token list  -> token  * token list)   // token

Ponieważ kod jest chroniony prawem autorskim, nie dołączę go tutaj, ale jest dostępny na Github . Nie kopiuj go tutaj.

Aby funkcje działały, należy pozostawić ogólne parametry, ale dołączam komentarze, które pokazują wywnioskowane typy w zależności od użycia funkcji. Ułatwia to zrozumienie funkcji konserwacji, pozostawiając ogólną funkcję do użycia.

Guy Coder
źródło
1
Dlaczego nie napisałeś wersji ogólnej w funkcji? Czy to nie zadziała?
svick,
@svick Nie rozumiem twojego pytania. Czy masz na myśli to, że pominąłem typy z definicji funkcji, ale mogłem dodać typy ogólne do definicji funkcji, ponieważ typy ogólne nie zmieniłyby znaczenia funkcji? Jeśli tak, powodem tego są osobiste preferencje. Kiedy zaczynałem od F #, dodałem je. Kiedy zacząłem pracować z bardziej zaawansowanymi użytkownikami F #, woleli pozostawić je jako komentarze, ponieważ łatwiej było zmodyfikować kod bez podpisów. ...
Guy Coder,
Na dłuższą metę sposób, w jaki pokazują je, ponieważ komentarze działały dla wszystkich, gdy kod działał. Kiedy zaczynam pisać funkcję, wstawiam typy. Gdy funkcja działa, przenoszę podpis typu na komentarz i usuwam jak najwięcej typów. Czasami z F # musisz zostawić ten typ, aby pomóc wnioskowanie. Przez jakiś czas eksperymentowałem z tworzeniem komentarza za pomocą let i =, abyś mógł odkomentować wiersz do testowania, a następnie komentować go ponownie po zakończeniu, ale wyglądały głupio i dodawanie let i = nie jest takie trudne.
Guy Coder,
Jeśli te komentarze odpowiadają na to, o co pytasz, daj mi znać, abym mógł przenieść je do odpowiedzi.
Guy Coder,
2
Więc kiedy modyfikujesz kod, nie modyfikujesz komentarzy, co prowadzi do nieaktualnej dokumentacji? Nie wydaje mi się to zaletą. I tak naprawdę nie rozumiem, jak niechlujny komentarz jest lepszy od niechlujnego kodu. Wydaje mi się, że takie podejście łączy wady obu opcji.
svick,
-8

Liczba błędów jest wprost proporcjonalna do liczby znaków w programie!

Zazwyczaj określa się to jako liczbę błędów proporcjonalną do linii kodu. Ale posiadanie mniejszej liczby wierszy z taką samą ilością kodu daje taką samą liczbę błędów.

Więc chociaż miło jest wyrazić się jasno, wszelkie dodatkowe parametry, które wpisujesz, mogą prowadzić do błędów.

James Anderson
źródło
5
„Liczba błędów jest wprost proporcjonalna do liczby znaków w programie!” Więc wszyscy powinniśmy przejść na APL ? Bycie zbyt zwięzłym to problem, tak jak bycie zbyt rozwlekłym.
svick,
5
Aby odpowiedzieć na to pytanie, załóżmy, że koszt dwuznaczności w umyśle programisty jest znacznie droższy niż kilka dodatkowych naciśnięć klawiszy.
JDB