Skip to content

Commit

Permalink
terminology fix
Browse files Browse the repository at this point in the history
  • Loading branch information
AnnaUdovichenko committed Jan 6, 2018
1 parent fe2d4ea commit fee503c
Showing 1 changed file with 10 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@

let listId<'T> : 'T list -> 'T list = List.map id

Это компилируется и работает так, как и ожидалось. На первый взгляд кажется, что это ошибка вывода типов - компилятор не может определить тип, поэтому мы добавили аннотацию, чтобы ему помочь. Но подождите, компилятор почти вывел этот тип - он же упоминается в сообщении об ошибке! (С таинственной переменной типа (type variable) '_a') Будто бы компилятор был ошарашен конкретно этим случаем - почему?
Это компилируется и работает так, как и ожидалось. На первый взгляд кажется, что это ошибка вывода типов - компилятор не может определить тип, поэтому мы добавили аннотацию, чтобы ему помочь. Но подождите, компилятор почти вывел этот тип - он же упоминается в сообщении об ошибке! (С таинственной ти́повой переменной (type variable) '_a') Будто бы компилятор был ошарашен конкретно этим случаем - почему?

По очень разумной причине. Чтобы увидеть её, давайте рассмотрим другой случай ограничения на значения. Эта ссылочная ячейка (reference cell) на список не скомпилируется:

Expand Down Expand Up @@ -78,13 +78,13 @@

(v<int>):=[1]

Левая сторона этого присваивания - это применение v к аргументу типа (type argument) int. А v, в свою очередь, это не ссылочкая ячейка, а функция типа: получив на входе аргумент типа, она вернёт ссылочную ячейку. Наше выражение создаёт новую ссылочную ячейку и присваивает ей "[1]". Аналогично, если мы явно укажем аргумент типа в разыменовании v:
Левая сторона этого присваивания - это применение v к аргументу типа (type argument) int. А v, в свою очередь, это не ссылочная ячейка, а ти́повая функция: получив на входе аргумент типа, она вернёт ссылочную ячейку. Наше выражение создаёт новую ссылочную ячейку и присваивает ей "[1]". Аналогично, если мы явно укажем аргумент типа в разыменовании v:

let x = !(v<int>)

мы увидим, что v снова применяется к аргументу типа, и возвращает свежую ссылочную ячейку, содержащую пустой список.

Чтобы конкретизировать разговор о функциях типа, давайте изучим полученный IL. Если мы скомпилируем определение v, наш верный Reflector покажет нам, что v это:
Чтобы конкретизировать разговор о ти́повых функциях, давайте изучим полученный IL. Если мы скомпилируем определение v, наш верный Reflector покажет нам, что v это:

public static FSharpRef<FSharpList<T>> v<T>(){
return Operators.Ref<FSharpList<T>>(FSharpList<T>.get_Empty());
Expand All @@ -100,12 +100,12 @@

Итак, когда безопасно автоматическое обобщение? Сложно привести точные критерии, но напрашивается один простой ответ: обобщение безопасно, когда правая часть let-выражения одновременно:

1. Не содержит *побочных эффектов* (иными словами, *чистое*)
1. Не содержит *побочных эффектов* (иными словами, *чистая*)
2. Возвращает *неизменяемый объект*

Действительно, причудливое поведение v возникает из-за изменяемости ссылочной ячейки; именно потому, что ссылочная ячейка изменяема, нам было важно, будет ли получена одна или разные ячейки в результате разных обращений к v. Если правая часть let-выражения не содержит побочных эффектов, мы знаем, что всегда получаем эквивалентные объекты, а так как они неизменяемы, нас не волнует, получаем ли мы оду и ту же или различные их копии в результате различных вызовов.

С точки зрения компилятора трудно - невозможно - точно установить, выполнены ли вышеупомянутые условия. Поэтому компилятор использует простое и грубое, но естественное и понятное приближение: он обобщает только тогда, когда может вывести чистоту и неизменяемость из синтаксической структуры выражения в правой части let. Поэтому:
С точки зрения компилятора трудно, даже невозможно точно установить, выполнены ли вышеупомянутые условия. Поэтому компилятор использует простое и грубое, но естественное и понятное приближение: он обобщает только тогда, когда может вывести чистоту и неизменяемость из синтаксической структуры выражения в правой части let. Поэтому:

let listId = fun l -> List.map id

Expand Down Expand Up @@ -166,7 +166,7 @@ f не будет обобщено. В примере выше gen - это бе
Either define 'l' as a simple data term,
make it a function with explicit arguments or,
if you do not intend for it to be generic, add a type annotation.
В самом деле, компилятор знает, что empty - это функция типа (type function), которая не подвергается автоматическому обобщению, так как она не принадлежит множеству синтаксических значений. F#, однако, предоставляет здесь лазейку - мы можем указать атрибут [<GeneralizableValue>] в определении empty:
В самом деле, компилятор знает, что empty - это ти́повая функция (type function), которая не подвергается автоматическому обобщению, так как она не принадлежит множеству синтаксических значений. F#, однако, предоставляет здесь лазейку - мы можем указать атрибут [<GeneralizableValue>] в определении empty:

[<GeneralizableValue>]
let empty<'T> : 'T lazylist = create (fun () -> Nil)
Expand All @@ -183,7 +183,7 @@ f не будет обобщено. В примере выше gen - это бе
let v<'T> : 'T list ref = ref []
Он полностью соответствует своему названию: теперь вы не можете написать “v := [1]”, только “v<int> := [1]”, и будет понятнее, что происходит на самом деле.

Если вы всё это уловили, я надеюсь, что у вас теперь есть чёткое понимание ограничения на значения в F#, и теперь вы можете при необходимости контролировать его с помощью явных аннотаций типов и аттрибута GeneralizableValue. Вместе с властью, однако, приходит и ответственность; статья на MSDN права - эти возможности редко используются в обыденном программировании на F#. В моём F# коде функции типа появляются только в случаях, аналогичных lazy list - базовых структурах данных (ground cases of data structures); во всех остальных случаях я следую советам из статьи на MSDN:
* Ограничить тип так, чтобы он перестал быть полиморфным, добавив явную аннотацию типа к значению или параметру.
* Если проблема в использовании необобщаемой конструкции для определения полиморфной функции, такой как композиция функций или частичное применение аргументов каррированной функции, попробуйте переписать определение функции на обыкновенное.
* Если проблема в том, что выражение слишком сложно для обобщения, превратите его в функцию, добавив неиспользуемый параметр.
Если вы всё это уловили, я надеюсь, что у вас теперь есть чёткое понимание ограничения на значения в F#, и теперь вы можете при необходимости контролировать его с помощью явных аннотаций типов и аттрибута GeneralizableValue. Вместе с властью, однако, приходит и ответственность; статья на MSDN права - эти возможности редко используются в обыденном программировании на F#. В моём F# коде ти́повые функции появляются только в случаях, аналогичных lazy list - базовых структурах данных (ground cases of data structures); во всех остальных случаях я следую советам из статьи на MSDN:
* Ограничьте тип так, чтобы он перестал быть полиморфным, добавив явную аннотацию типа к значению или параметру.
* Если проблема заключается в использовании необобщаемой конструкции для определения полиморфной функции, такой как композиция функций или частичное применение аргументов каррированной функции, попробуйте переписать определение функции на обыкновенное.
* Если проблема заключается в том, что выражение слишком сложно для обобщения, превратите его в функцию, добавив неиспользуемый параметр.

0 comments on commit fee503c

Please sign in to comment.