2013-04-15 2 views
7

Существует несколько случаев, когда F поведение # записей странно мне:Странное поведение F # записей

Нет предупреждения о неоднозначности

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";} 

Предупреждение о записей деконструкции вместо построения записей

Вместо того, чтобы получать предупреждение о конструкции записей в предыдущем случае, компилятор F # выпустил предупреждение о записях «деконструкция»:

// 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 

Обратите внимание, что не существует никаких предупреждений с сопоставлением с образцом (я подозреваю, то потому что модели построены с использованием «строительные записями выражения», а не «запись деконструкции выражения»):

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

Типа ошибка вывода с отсутствующим полем

Компилятор F # выберет второй тип и выдаст ошибку, потому что поле Возраста отсутствует!

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";} 

Уродливый синтаксис «записей деконструкции»

Я попросил несколько коллег и мой вопрос: «Что этот код все?»

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

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

Это было полной неожиданностью для всех, что «ID» и «имя» на самом деле «lvalues», хотя они поместили в «правой» части выражения. Я понимаю, что это гораздо больше касается личных предпочтений, но для большинства людей кажется странным, что в одном конкретном случае выходные значения располагаются в правой части выражения.

Я не думаю, что все это ошибки, я подозреваю, что большая часть этого материала на самом деле есть функции.
Мой вопрос: Существует ли какое-либо разумное позади такое неясное поведение?

ответ

6

Ваши примеры можно разделить на две категории: записей выражений и записей моделей. В то время как для выражений записи требуется объявить все поля и вернуть некоторые выражения, шаблоны записи имеют необязательные поля и предназначены для сопоставления шаблонов. The MSDN page on Records имеют два четких раздела на них, это может быть полезно прочитать.

В этом примере

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";} 

поведение ясно из правила, указанного в the MSDN page above.

Этикетки самого последнего заявленного типа имеют приоритет над тех ранее объявленного типа

В случае сравнения с шаблоном, вы сосредоточены на создании некоторых привязок вам нужно. Таким образом, вы можете написать

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

для того, чтобы получить id связывания для последующего использования. Шаблон соответствия на выпускаемую привязок может выглядеть немного странно, но это очень похоже на то, как вы обычно сопоставление с образцом в параметрах функции:

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

Я думаю, что предупреждения на вашем шаблоне соответствующие примеры оправданы, потому что компилятор не может угадать ваше намерение.

Тем не менее, разные записи с повторяющимися полями не рекомендуется. По крайней мере, вы должны использовать квалифицированные имена, чтобы избежать путаницы:

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

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

Спасибо, @pad. Я понимаю, что текущее поведение для принятия последнего определения хорошо документировано. Но мне все же кажется странным. Я предпочитаю получать ошибку или предупреждение в этом случае. А как насчет моего раздела под названием «Ошибка ввода типа с отсутствующим полем»? Вам кажется разумным? –

5

Я думаю, что большинство из ваших комментариев связаны с тем, что запись имена становятся доступны непосредственно в пространстве имен, где определен записи - то есть, когда вы определяете a запись Person со свойствами Name и Id, имена Name и Id видны во всем мире. Это имеет как преимущества, так и недостатки:

  • Хорошая вещь, что делает программирование легче, потому что вы можете просто написать {Id=1; Name="bob"}
  • Плохо то, что имена могут конфликтовать с другими именами записей, которые находятся в области видимости и поэтому, если ваши имена не уникальны (ваш первый пример), вы попадаете в беду.

Вы можете сообщить компилятору, что хотите, чтобы явным образом квалифицировал имя, используя атрибут RequireQualifiedAccess. Это означает, что вы не сможете писать только Id или Name, но вы должны всегда включать имя типа:

[<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" } 

Это дает более строгий режим, но это делает программирование более удобным. По умолчанию (немного более неоднозначное поведение) уже объясняется @pad - компилятор просто выберет имя, определенное позже в вашем источнике. Он делает это даже в том случае, если он может вывести тип, просмотрев другие поля в выражении - просто потому, что поиск других полей не всегда будет работать (например, когда вы используете ключевое слово with), поэтому лучше придерживаться простая последовательная стратегия.

Что касается соответствия шаблону, я был очень смущен, когда впервые увидел синтаксис. Я думаю, что он не используется очень часто, но он может быть полезен.

Важно понимать, что F # не использует структурную типизацию (это означает, что вы не можете использовать запись с большим количеством полей в качестве аргумента функции, которая принимает запись с меньшим количеством полей). Это может быть полезной функцией, но она не подходит для системы типа .NET. Это в основном означает, что вы не можете ожидать слишком причудливых вещей - аргумент должен быть записью известного типа записи.

При записи:

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

Термина именующего относится к тому факту, что id и name появляются в шаблоне, а не в выражения. Определение синтаксиса в F # говорит вам что-то вроде этого:

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

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

Итак, левая рука = в let представляет собой шаблон, в то время как правая рука является выражением. Две аналогичные структуры в F # - (x, y) могут использоваться как для построения, так и для деконструкции кортежа. И то же самое касается записей ...

+0

Спасибо, Томас. Ясно, что это особенность, но не ошибка.) Я хочу сказать: почему у нас есть текущее поведение? Например, если компилятор видит два подходящих типа записи, я предпочел бы увидеть это (по крайней мере, с предупреждением). А как насчет раздела «Ошибка ввода типа с отсутствующим полем»? Вам кажется разумным? –

+0

@Sergey: Почему следует вводить теневое предупреждение для предупреждения или ошибки при отсутствии переменной тени? Действительно, последний является идиоматическим, не удивительным ... – ildjarn

+0

@ildjarn: предположим, у нас большой проект, и кто-то добавит в него подобную запись. Компилятор выберет последнюю версию, относящуюся к порядку файла в проводнике решений. Для меня это слишком опасно. Почему * не выдавать предупреждение в этом случае *? Какая польза от того, чтобы не показывать предупреждение пользователю? –

Смежные вопросы