2013-04-15 21 views
7

Istnieje kilka przypadków, kiedy F # rekordy zachowanie jest dla mnie dziwne:Dziwne zachowanie F # zapisów

Brak ostrzeżenia o dwuznaczności

type AnotherPerson = {Id: int; Name: string} 
type Person = {Id: int; Name: string;} 

// F# compiler will use second type without any complains or warnings 
let p = {Id = 42; Name = "Foo";} 

Ostrzeżenie na rekordy dekonstrukcji zamiast budowy rekordy

Zamiast otrzymywać ostrzeżenie o tworzeniu rekordów w poprzednim przypadku, kompilator F # wydał ostrzeżenie o "dekonstrukcji" rekordów:

// Using Person and AnotherPerson types and "p" from the previous example! 
// We'll get a warning here: "The field labels and expected type of this 
// record expression or pattern do not uniquely determine a corresponding record type" 
let {Id = id; Name = name} = p 

Zauważ, że nie ma ostrzeżenia z dopasowywanie wzorca (podejrzewam To dlatego, że wzory są zbudowane przy użyciu „wyrażeń budowlanych rekordy”, a nie „rekordy dekonstrukcji wyrażenie”):

match p with 
| {Id = _; Name = "Foo"} -> printfn "case 1" 
| {Id = 42; Name = _} -> printfn "case 2" 
| _ -> printfn "case 3" 

Rodzaj błąd wnioskowania z brakującym polem

F # kompilator wybierze drugi typ, a następnie spowoduje błąd, ponieważ brakuje pola Wiek!

type AnotherPerson = {Id: int; Name: string} 
type Person = {Id: int; Name: string; Age: int} 

// Error: "No assignment given for field 'Age' of type 'Person'" 
let p = {Id = 42; Name = "Foo";} 

brzydki składni dla „rekordy” dekonstrukcji

poprosiłem kilka koledzy z kopalni pytanie: „Co to jest kod chodzi?”

type Person = {Id: int; Name: string;} 
let p = {Id = 42; Name = "Foo";} 

// What will happend here? 
let {Id = id; Name = name} = p 

To było całkowite zaskoczenie dla wszystkich, że „id” i „name” są rzeczywiście „lwartościami” choć umieszczone w „prawej” w wyrażeniu. Rozumiem, że chodzi o coś więcej na osobiste preferencje, ale dla większości ludzi wydaje się dziwne, że w jednym przypadku wartości wyjściowe są umieszczone po prawej stronie wyrażenia.

Nie sądzę, że to wszystko to błędy, podejrzewam, że większość z tych rzeczy to w rzeczywistości funkcje.
Moje pytanie brzmi: Czy istnieje jakieś racjonalne uzasadnienie takiego mrocznego zachowania?

Odpowiedz

6

Twoje przykłady można podzielić na dwie kategorie: wyrażenia płytowych i wzorców płytowych. Podczas gdy rekordowe wyrażenia wymagają zadeklarowania wszystkich pól i zwrócenia niektórych wyrażeń, wzorce nagrań mają opcjonalne pola i są w celu dopasowania wzorca. The MSDN page on Records mają dwie wyraźne sekcje, które mogą być warte przeczytania.

W tym przykładzie

type AnotherPerson = {Id: int; Name: string} 
type Person = {Id: int; Name: string;} 

// F# compiler will use second type without any complains or warnings 
let p = {Id = 42; Name = "Foo";} 

zachowanie wynika z reguły podanej w the MSDN page above.

Etykiety ostatnio zadeklarowanego typu mają pierwszeństwo przed tych wcześniej zadeklarowanego typu

W przypadku dopasowywania wzorców, skupić się na tworzeniu pewnych powiązań, których potrzebujesz. Więc można napisać

type Person = {Id: int; Name: string;} 
let {Id = id} = p 

w celu uzyskania id wiążące dla późniejszego wykorzystania. Wzorzec dopasowania na powiązaniach let może wyglądać nieco dziwnie, ale to jest bardzo podobne do sposobu zwykle wzór mecz w parametrach funkcyjnych:

type Person = {Id: int; Name: string;} 
let extractName {Name = name} = name 

myślę ostrzeżenia na swojej wzór dopasowania przykłady są uzasadnione, ponieważ kompilator nie może odgadnąć twój zamiar.

Mimo to różne rekordy z duplikatami pól nie są zalecane. Przynajmniej należy użyć nazwy kwalifikowane, aby uniknąć nieporozumień:

type AnotherPerson = {Id: int; Name: string} 
type Person = {Id: int; Name: string; Age: int} 

let p = {AnotherPerson.Id = 42; Name = "Foo"} 
+0

Dzięki, @pad. Rozumiem, że obecne zachowanie do podjęcia ostatniej definicji jest dobrze udokumentowane. Ale nadal wydaje mi się to dziwne. W tym przypadku wolę błąd lub ostrzeżenie. A co z moją sekcją "Błąd wnioskowania z brakującym polem"? Czy wydaje ci się to rozsądne? –

5

Myślę, że większość twoich komentarzach są związane z faktem, że nazwy rekord stać bezpośrednio dostępne w przestrzeni nazw, gdzie płyta jest zdefiniowany - czyli podczas definiowania rekord Person o właściwościach Name i Id, globalnie widoczne są nazwy Name i Id. To ma zarówno zalety, jak i wady:

  • Dobrą rzeczą jest to, że sprawia, że ​​programowanie łatwiej, bo można po prostu napisać {Id=1; Name="bob"}
  • Złą rzeczą jest to, że nazwy mogą kolidować z innymi nazwami płytowych, które są w zakresie i więc jeśli twoje imiona nie są unikalne (twój pierwszy przykład), wpadasz w kłopoty.

Możesz powiedzieć kompilatorowi, że chcesz zawsze jawnie zakwalifikować nazwę przy użyciu atrybutu RequireQualifiedAccess. Oznacza to, że nie będzie mógł napisać tylko Id lub Name, ale trzeba zawsze podawać nazwę typu:

[<RequireQualifiedAccess>] 
type AnotherPerson = {Id: int; Name: string} 
[<RequireQualifiedAccess>] 
type Person = {Id: int; Name: string;} 

// You have to use `Person.Id` or `AnotherPerson.Id` to determine the record 
let p = {Person.Id = 42; Person.Name = "Foo" } 

To daje surowszy tryb, ale to sprawia, że ​​programowanie mniej wygodne. Domyślne (nieco bardziej niejednoznaczne zachowanie) jest już wyjaśnione przez @pad - kompilator po prostu wybierze nazwę zdefiniowaną później w twoim źródle. Czyni to nawet w przypadku, gdy mógłby wywnioskować typ, patrząc na inne pola w wyrażeniu - po prostu dlatego, że patrzenie na inne pola nie zawsze działa (np. Gdy używasz słowa kluczowego with), więc lepiej jest trzymać się prosta, spójna strategia.

Jeśli chodzi o dopasowywanie wzorców, byłem bardzo zdezorientowany, gdy po raz pierwszy zobaczyłem składnię. Myślę, że nie jest to często używane, ale może być przydatne.

Ważne jest, aby zdać sobie sprawę, że F # nie używa strukturalnego typowania (co oznacza, że ​​nie można użyć rekordu z większą ilością pól jako argumentu dla funkcji, która przyjmuje rekord z mniejszą liczbą pól). Może to być przydatna funkcja, ale nie pasuje ona do systemu .NET. Zasadniczo oznacza to, że nie można oczekiwać zbyt fantazyjnych rzeczy - argument musi być zapisem znanego, nazwanego typu rekordu.

Kiedy piszesz:

let {Id = id; Name = name} = p 

Termin lwartość odnosi się do faktu, że id i name pojawiają się w wzór zamiast w ekspresji. Definicja składni w F # mówi coś takiego:

expr := let <pat> = <expr> 
     | { id = <expr>; ... } 
     | <lots of other expressions> 

pat := id 
     | { id = <pat>; ... } 
     | <lots of other patterns> 

więc lewa strona od = w let jest wzór, natomiast prawa strona jest wyrazem. Obie mają podobną strukturę w F # - (x, y) mogą być używane zarówno do konstruowania, jak i do dekompilacji krotki. Podobnie jest w przypadku nagrań ...

+0

Dzięki, Tomas. Jest jasne, że jest to funkcja, ale nie błąd;) Chodzi mi o to, dlaczego mamy obecne zachowanie? Na przykład, jeśli kompilator zobaczy dwa pasujące typy rekordów, wolę to zobaczyć (przynajmniej z ostrzeżeniem). A co z sekcją "Typ błędu inferencyjnego z brakującym polem"? Czy wydaje ci się to rozsądne? –

+0

@Sergey: Dlaczego wpisywanie w cień ma gwarantować ostrzeżenie/błąd, gdy nie ma opcji cieniowania zmiennego? W istocie ta ostatnia jest idiomatyczna, nie zaskakuje ... – ildjarn

+0

@ildjarn: załóżmy, że mamy duży projekt i ktoś doda do niego podobny rekord. Kompilator wybierze najnowszy związany z kolejnością plików w eksploratorze rozwiązań. Dla mnie jest to zbyt niebezpieczne. Dlaczego * nie wydawać ostrzeżenia w tym przypadku *? Jakie korzyści wynikają z braku ostrzeżenia dla użytkownika? –