Skip to content

Commit

Permalink
Small edit
Browse files Browse the repository at this point in the history
  • Loading branch information
AnnaUdovichenko committed Jan 6, 2018
1 parent 1909088 commit fe2d4ea
Showing 1 changed file with 9 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

Алгоритмы автоматического вывода типов - захватывающая тема, за ними стоит интересная и красивая теория. Сегодня мы рассмотрим один интересный аспект автоматического вывода типов в F#, который, возможно, даст вам представление о том, какие сложности возникают в хороших современных алгоритмах такого рода, и, надеюсь, объяснит один камень преткновения, с которым время от времени сталкиваются F# программисты.

Нашей темой сегодня будет *ограничение на значения (value restriction)*. На MSDN есть [хорошая статья]("http://msdn.microsoft.com/en-us/library/dd233183(v=VS.100).aspx") на тему ограничения на значения и автоматического обобщения типов в F#, которая даёт очень разумные практические советы, что делать, если вы столкнулись с этим в вашем коде. Однако, эта статья просто как ни в чём не бывало констатирует: "*Компилятор выполняет автоматическое обобщение только на полных определениях функций с явно указанными аргументами и на простых неизменяемых значениях*", и не даёт этому практически никаких обоснований, что вполне справедливо, потому что MSDN - это просто справочный материал. Мой пост сфокусирован на рассуждениях, лежащих в основе ограничения на значения - я буду отвечать на вопрос "почему?", а не "что делать?".
Нашей темой сегодня будет *ограничение на значения (value restriction)*. На MSDN есть [хорошая статья]("http://msdn.microsoft.com/en-us/library/dd233183(v=VS.100).aspx") на тему ограничения на значения и автоматического обобщения типов в F#, которая даёт очень разумные практические советы, что делать, если вы столкнулись с ним в вашем коде. Однако, эта статья просто как ни в чём не бывало констатирует: "*Компилятор выполняет автоматическое обобщение только на полных определениях функций с явно указанными аргументами и на простых неизменяемых значениях*", и не даёт этому практически никаких обоснований, что вполне справедливо, потому что MSDN - это просто справочный материал. Мой пост сфокусирован на рассуждениях, лежащих в основе ограничения на значения - я буду отвечать на вопрос "почему?", а не "что делать?".

*Автоматическое обобщение (automatic generalization)* - мощная возможность автоматического вывода типов F#. Определим простую функцию, например функцию тождества:

Expand All @@ -17,6 +17,7 @@
(не очень полезная функция, но полезный код - это не сегодняшняя моя цель). Компилятор F# даёт функции listId корректный тип 'a list -> 'a list; снова произошло автоматическое обобщение. Но поскольку List.map - каррированная функция, у нас может возникнуть искушение отбросить аргумент l слева и справа:

let listId = List.map id

Но F# компилятор неожиданно возмущается:

Program.fs(8,5): error FS0030: Value restriction.
Expand All @@ -28,7 +29,7 @@
Что происходит?

Статья на MSDN предлагает 4 способа исправить let-определение, которое отвергается компилятором из-за ограничения на значения:
* Ограничить тип так, чтобы он перестал быть обобщённым, добавив явную аннотацию типа к значению или параметру.
* Ограничить тип так, чтобы он перестал быть полиморфным, добавив явную аннотацию типа к значению или параметру.
* Если проблема заключается в необобщаемой конструкции (nongeneralizable construct) в определении полиморфной функции (такой, как композиция функций или неполное применение аргументов к каррированной функции), попробуйте переписать определение функции на обыкновеное
* Если проблема заключается в выражении, слишком сложном для обобщения, превратите его в функцию, добавив неиспользуемый параметр.
* Добавьте явные полиморфные типы-параметры. Это способ используется редко.
Expand Down Expand Up @@ -102,9 +103,9 @@
1. Не содержит *побочных эффектов* (иными словами, *чистое*)
2. Возвращает *неизменяемый объект*

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

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

let listId = fun l -> List.map id

Expand All @@ -120,14 +121,14 @@
type 'a r = { x : 'a; y : int }
let r1 = { x = []; y = 1 }

r1 получает тип 'a list r. Однако, если вы попытаетесь проинициализировать какие-либо поля неизменяемой записи вызовом функции:
r1 получает тип 'a list r. Однако, если вы попытаетесь проинициализировать какие-либо поля неизменяемой записи результатом вызова функции:

let gen =
let u = ref 0
fun () -> u := !u + 1; !u
let f = { x = []; y = gen() }

f не будет обобщено. В примере выше gen - это безусловно грязная (non-pure) функция; она могла бы быть чистой, но компилятор не может об этом знать, поэтому он из предосторожности возвращает ошибку. По этой же причине,
f не будет обобщено. В примере выше gen - это безусловно грязная (non-pure) функция; она могла бы быть чистой, но компилятор не может об этом знать, поэтому он из предосторожности возвращает ошибку. По этой же причине

let listId = List.map id
не обобщается - компилятор не знает, чистая функция List.map или нет.
Expand Down Expand Up @@ -180,9 +181,9 @@ f не будет обобщено. В примере выше gen - это бе

[<RequiresExplicitTypeArguments>]
let v<'T> : 'T list ref = ref []
Он делает именно то, что и говорит: теперь вы не можете написать “v := [1]”, только “v<int> := [1]”, и будет понятнее, что происходит на самом деле.
Он полностью соответствует своему названию: теперь вы не можете написать “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 fe2d4ea

Please sign in to comment.