From d06c87396050de4101eea9a2c02e87d101b7f805 Mon Sep 17 00:00:00 2001 From: Andrei Kurosh Date: Tue, 9 Jan 2018 13:32:17 +0300 Subject: [PATCH] Minor refactoring. --- .../Finer Points of F# Value Restriction.md | 95 ++++++++++--------- 1 file changed, 51 insertions(+), 44 deletions(-) diff --git a/Finer Points of F# Value Restriction/Finer Points of F# Value Restriction.md b/Finer Points of F# Value Restriction/Finer Points of F# Value Restriction.md index d625e9a..6a63293 100644 --- a/Finer Points of F# Value Restriction/Finer Points of F# Value Restriction.md +++ b/Finer Points of F# Value Restriction/Finer Points of F# Value Restriction.md @@ -1,20 +1,21 @@ # Тонкости value restriction в F# Одной из отличительных особенностей языка F#, по сравнению с более распространёнными языками программирования, является мощный и всеобъемлющий автоматический вывод типов. Благодаря ему в программах на F# вы почти никогда не указываете типы явно, набираете меньше текста, и получаете в итоге более краткий, фантастически элегантный код. -Алгоритмы автоматического вывода типов - захватывающая тема, за ними стоит интересная и красивая теория. Сегодня мы рассмотрим один интересный аспект автоматического вывода типов в F#, который, возможно, даст вам представление о том, какие сложности возникают в хороших современных алгоритмах такого рода, и, надеюсь, объяснит один камень преткновения, с которым время от времени сталкиваются F# программисты. +Алгоритмы автоматического вывода типов - захватывающая тема; за ними стоит интересная и красивая теория. Сегодня мы рассмотрим один любопытный аспект автоматического вывода типов в 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#. Определим простую функцию, например функцию тождества: +*Автоматическое обобщение (automatic generalization)* - мощная возможность автоматического вывода типов F#. Определим простую функцию - например, функцию тождества: let id x = x -Здесь нет явных аннотаций типов (type annotations), но компилятор F# выводит для этой функции тип 'a -> 'a - функция принимает аргумент некоего типа и возвращает результат точно такого же типа. Это не особенно сложно, но заметьте, что компилятор F# вывел неявный тип-параметр (generic type parameter) 'a для функции id. +Здесь нет явных аннотаций типов (type annotations), но компилятор F# выводит для этой функции тип `'a -> 'a`: функция принимает аргумент некоего типа и возвращает результат точно такого же типа. Это не особенно сложно, но заметьте, что компилятор F# вывел неявный тип-параметр (generic type parameter) `'a` для функции `id`. -Мы можем скомбинировать функцию id с функцией List.map, которая сама по себе полиморфна (generic function): +Мы можем скомбинировать функцию `id` с функцией `List.map`, которая сама по себе полиморфна (generic function): let listId l = List.map id l -(не очень полезная функция, но полезный код - это не сегодняшняя моя цель). Компилятор F# даёт функции listId корректный тип 'a list -> 'a list; снова произошло автоматическое обобщение. Но поскольку List.map - каррированная функция, у нас может возникнуть искушение отбросить аргумент l слева и справа: + +(Не очень полезная функция, но сейчас важнее наглядность). Компилятор F# даёт функции `listId` корректный тип `'a list -> 'a list` - снова произошло автоматическое обобщение. Поскольку `List.map` - каррированная функция, у нас может возникнуть искушение отбросить аргумент `l` слева и справа: let listId = List.map id @@ -28,29 +29,27 @@ Что происходит? -Статья на MSDN предлагает 4 способа исправить let-определение, которое отвергается компилятором из-за ограничения на значения: +Статья на MSDN предлагает 4 способа исправить `let`-определение, которое отвергается компилятором из-за ограничения на значения: * Ограничить тип так, чтобы он перестал быть полиморфным, добавив явную аннотацию типа к значению или параметру. -* Если проблема заключается в необобщаемой конструкции (nongeneralizable construct) в определении полиморфной функции (такой, как композиция функций или неполное применение аргументов к каррированной функции), попробуйте переписать определение функции на обыкновеное +* Если проблему вызывает необобщаемая конструкция (nongeneralizable construct) в определении полиморфной функции (такой, как композиция функций или частичное применение аргументов к каррированной функции), попробуйте переписать определение функции на обыкновеное. * Если проблема заключается в выражении, слишком сложном для обобщения, превратите его в функцию, добавив неиспользуемый параметр. -* Добавьте явные полиморфные типы-параметры. Это способ используется редко. +* Добавьте явные полиморфные типы-параметры. Этот способ используется редко. -Первый способ не для нас - мы хотим, чтобы функция listId была полиморфной. +Первый способ не для нас - мы хотим, чтобы функция `listId` была полиморфной. -Второй способ вернёт нас к явному указанию параметра list - и это канонический способ определения функций вроде listId в языке F#. +Второй способ вернёт нас к явному указанию параметра `list` - и это канонический способ определения функций вроде `listId` в языке F#. -Способ 3 применим, когда нужно определить что-то, не являющееся функцией, в нашем случае это даёт +Способ 3 применим, когда нужно определить что-то, не являющееся функцией, в нашем случае это даёт неубедительный вариант: let listId () = List.map id -что неубедительно. - В рабочем коде я бы поспользовался вторым способом и оставил параметр функции явным. Но ради обучения давайте попробуем "редко используемый" четвёртый способ: let listId<'T> : 'T list -> 'T list = List.map id -Это компилируется и работает так, как и ожидалось. На первый взгляд кажется, что это ошибка вывода типов - компилятор не может определить тип, поэтому мы добавили аннотацию, чтобы ему помочь. Но подождите, компилятор почти вывел этот тип - он же упоминается в сообщении об ошибке! (С таинственной ти́повой переменной (type variable) '_a') Будто бы компилятор был ошарашен конкретно этим случаем - почему? +Это компилируется и работает так, как и ожидалось. На первый взгляд кажется, что это ошибка вывода типов - компилятор не может определить тип, поэтому мы добавили аннотацию, чтобы ему помочь. Но подождите, компилятор почти вывел этот тип - он же упоминается в сообщении об ошибке (с таинственной ти́повой переменной (type variable) `'_a`)! Будто бы компилятор был ошарашен конкретно этим случаем - почему? -По очень разумной причине. Чтобы увидеть её, давайте рассмотрим другой случай ограничения на значения. Эта ссылочная ячейка (reference cell) на список не скомпилируется: +Причина вполне разумна. Чтобы увидеть её, давайте рассмотрим другой случай ограничения на значения. Эта ссылочная ячейка (reference cell) на список не скомпилируется: let v = ref [] Program.fs(16,5): error FS0030: Value restriction. @@ -64,52 +63,54 @@ >let v<'T> : 'T list ref = ref [] val v<'T> : 'T list ref -Компилятор доволен. Давайте попробуем присвоить v какое-нибудь значение: +Компилятор доволен. Давайте попробуем присвоить `v` какое-нибудь значение: > v := [1];; val it : unit = () -Правда же, v теперь ссылается на список с единственным элементом 1? + +Правда же, `v` теперь ссылается на список с единственным элементом `1`? > let x : int list = !v;; val x : int list = [] -Упс! Содержимое v - пустой список! Куда делся наш [1]? + +Ой! Содержимое `v` - пустой список! Куда делся наш `[1]`? Вот что произошло. Наше присваивание на самом деле может быть переписано так: (v):=[1] -Левая сторона этого присваивания - это применение v к аргументу типа (type argument) int. А v, в свою очередь, это не ссылочная ячейка, а ти́повая функция: получив на входе аргумент типа, она вернёт ссылочную ячейку. Наше выражение создаёт новую ссылочную ячейку и присваивает ей "[1]". Аналогично, если мы явно укажем аргумент типа в разыменовании v: +Левая сторона этого присваивания - это применение `v` к аргументу типа (type argument) `int`. А `v`, в свою очередь, это не ссылочная ячейка, а ти́повая функция: получив на входе аргумент типа, она вернёт ссылочную ячейку. Наше выражение создаёт новую ссылочную ячейку и присваивает ей `[1]`. Аналогично, если мы явно укажем аргумент типа в разыменовании `v`: let x = !(v) -мы увидим, что v снова применяется к аргументу типа, и возвращает свежую ссылочную ячейку, содержащую пустой список. +мы увидим, что `v` снова применяется к аргументу типа, и возвращает свежую ссылочную ячейку, содержащую пустой список. -Чтобы конкретизировать разговор о ти́повых функциях, давайте изучим полученный IL. Если мы скомпилируем определение v, наш верный Reflector покажет нам, что v это: +Чтобы конкретизировать разговор о ти́повых функциях, давайте изучим полученный IL-код. Если мы скомпилируем определение `v`, наш верный Reflector покажет нам, что `v` это: public static FSharpRef> v(){ return Operators.Ref>(FSharpList.get_Empty()); } -То, что мы воспринимаем как значение в F#, на самом деле является полиморфным методом без параметров в соответствующем IL. И присваивание, и разыменование v вызывают IL метод, который будет каждый раз возвращать новую ссылочную ячейку. +То, что мы воспринимаем как значение в F#, на самом деле является обобщенным методом без параметров в соответствующем IL-коде. И присваивание, и разыменование `v` вызывают IL-метод, который будет каждый раз возвращать новую ссылочную ячейку. -Однако, ничего в выражении +Однако же, ничто в выражении let v = ref [] -не намекает на подобное поведение. v выглядит как обыкновенное значение, а вовсе не метод и даже не функция. Если бы подобное определение было разрешено, F# разработчиков поджидал бы неприятный сюрприз. Вот поэтому компилятор перестаёт выводить здесь полиморфные параметры - ограничение на значения оберегает вас от неожиданного поведения. +не намекает на подобное поведение. Имя `v` выглядит как обыкновенное значение, а вовсе не метод и даже не функция. Если бы подобное определение было разрешено, F#-разработчиков ожидал бы неприятный сюрприз. Вот поэтому здесь компилятор перестаёт выводить полиморфные параметры - ограничение на значения оберегает вас от неожиданного поведения. -Итак, когда безопасно автоматическое обобщение? Сложно привести точные критерии, но напрашивается один простой ответ: обобщение безопасно, когда правая часть let-выражения одновременно: +Итак, когда безопасно автоматическое обобщение? Сложно привести точные критерии, но напрашивается один простой ответ: обобщение безопасно, когда правая часть `let`-выражения одновременно: 1. Не содержит *побочных эффектов* (иными словами, *чистая*) 2. Возвращает *неизменяемый объект* -Действительно, причудливое поведение v возникает из-за изменяемости ссылочной ячейки; именно потому, что ссылочная ячейка изменяема, нам было важно, будет ли получена одна или разные ячейки в результате разных обращений к v. Если правая часть let-выражения не содержит побочных эффектов, мы знаем, что всегда получаем эквивалентные объекты, а так как они неизменяемы, нас не волнует, получаем ли мы оду и ту же или различные их копии в результате различных вызовов. +Действительно, причудливое поведение `v` возникает из-за изменяемости ссылочной ячейки; именно потому, что ссылочная ячейка изменяема, нам было важно, будет ли получена одна или разные ячейки в результате разных обращений к `v`. Если правая часть `let`-выражения не содержит побочных эффектов, мы знаем, что всегда получаем эквивалентные объекты, а так как они неизменяемы, нас не волнует, получаем ли мы одну и ту же или различные их копии в результате различных вызовов. -С точки зрения компилятора трудно, даже невозможно точно установить, выполнены ли вышеупомянутые условия. Поэтому компилятор использует простое и грубое, но естественное и понятное приближение: он обобщает только тогда, когда может вывести чистоту и неизменяемость из синтаксической структуры выражения в правой части let. Поэтому: +С точки зрения компилятора трудно, даже невозможно точно установить, выполнены ли вышеупомянутые условия. Поэтому компилятор использует простое и грубое, но естественное и понятное приближение: он обобщает только тогда, когда может вывести чистоту и неизменяемость из синтаксической структуры выражения в правой части `let`. Поэтому: let listId = fun l -> List.map id -(то, для чего наше оригинальное определение “let listId l = List.map id l” является синтаксическим сахаром) обобщается - в правой части создание замыкания; создание замыкания не содержит побочных эффектов и замыкания неизменяемы. +(то, для чего наше оригинальное определение `let listId l = List.map id l` является синтаксическим сахаром) обобщается - в правой части создание замыкания; создание замыкания не содержит побочных эффектов и замыкания неизменяемы. Аналогично с непересекающимися объединениями: @@ -121,29 +122,31 @@ 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 или нет. -Выражения, для которых компилятор на уровне синтаксиса может определить, что они чистые и возвращают неизменяемые объекты, называются *синтаксическими значениями*. Так ограничение на значения получило своё название - *автоматическое обобщение правой части let-выражения ограничено синтаксическими значениями*. Описание языка F# содержит полный список синтаксических значений, но наше обсуждение даёт представление о том, что это за выражения - чистые и возвращающие неизменяемые объекты. +не обобщается - компилятор не знает, чистая функция `List.map` или нет. + +Выражения, для которых компилятор на уровне синтаксиса может определить, что они чистые и возвращают неизменяемые объекты, называются *синтаксическими значениями*. Так ограничение на значения получило своё название - *автоматическое обобщение правой части `let`-выражения ограничено синтаксическими значениями*. Описание языка F# содержит полный список синтаксических значений, но наше обсуждение даёт представление о том, что это за выражения - чистые и возвращающие неизменяемые объекты. -Задача, которую мы здесь решаем, не нова - все компиляторы языков семейства ML используют ограничение на значения в той или иной форме. Особенностью F#, котрую я считаю уникальной, является то, что ограничение на значения можно обойти с помощью явных аннотаций типов, и это безопасно с точки зрения семантики F#. +Задача, которую мы здесь решаем, не нова - все компиляторы языков семейства ML используют ограничение на значения в той или иной форме. Уникальной же, на мой взгляд, особенностью F# является возможность обойти ограничение на значения с помощью явных аннотаций типов, и это безопасно с точки зрения семантики F#. -Когда это может быть полезно? Классический пример это lazy и lazy list. Типичное определение lazy (давайте притворимся, что его нет в нашем языке) +Когда это может быть полезно? Классический пример - `lazy` и `lazy list`. Типичное определение `lazy` (давайте притворимся, что его нет в нашем языке) type 'a LazyInner = Delayed of (unit -> 'a) | Value of 'a | Exception of exn type 'a Lazy = 'a LazyInner ref let create f = ref (Delayed f) let force (l : 'a Lazy) = ... -на первый взгляд, полно побочных эффектов; компилятору не известен контракт между create и force. Если мы построим lazy list обычным способом с помощью определения lazy + +на первый взгляд, полно побочных эффектов; компилятору не известен контракт между `create` и `force`. Если мы построим `lazy list` обычным способом с помощью определения `lazy` type 'a cell = Nil | CCons of 'a * 'a lazylist and 'a lazylist = 'a cell Lazy @@ -155,9 +158,11 @@ f не будет обобщено. В примере выше gen - это бе ограничение на значения не позволит нам это сделать; однако полиморфное использование ленивого списка абсолютно законно; мы можем заявить об этом, явно указав параметр полиморфного типа: let empty<'T> : 'T lazylist = create (fun () -> Nil) -Этого достаточно, чтобы определение empty скомпилировалось, но если мы попытаемся его использовать: + +Этого достаточно, чтобы определение `empty` скомпилировалось, но если мы попытаемся его использовать: let l = empty + компилятор снова возмутится: File1.fs(12,5): error FS0030: Value restriction. @@ -166,24 +171,26 @@ 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#, однако, предоставляет здесь лазейку - мы можем указать атрибут [] в определении empty: + +В самом деле, компилятор знает, что `empty` - это ти́повая функция (type function), которая не подвергается автоматическому обобщению, так как она не принадлежит множеству синтаксических значений. F#, однако, предоставляет здесь лазейку - мы можем указать атрибут `[]` в определении empty: [] let empty<'T> : 'T lazylist = create (fun () -> Nil) -это заставит компилятор считать empty синтаксическим значением, и "let l = empty" скомпилируется. +Это заставит компилятор считать `empty` синтаксическим значением, и выражение `let l = empty` скомпилируется. -На самом деле, иногда определения вроде нашего полиморфного v могут быть полезными: +На самом деле, иногда определения вроде нашего полиморфного `v` могут быть полезными: let v<'T> : 'T list ref = ref [] -Если вы пишете функцию, параметризуемую типами (type-parametrized), и возвращающую изменяемые объекты или имеющую побочные эффекты, укажите атрибут RequiresExplicitTypeArguments: +Если вы пишете функцию, параметризуемую типами (type-parametrized) и возвращающую изменяемые объекты или имеющую побочные эффекты, укажите атрибут `RequiresExplicitTypeArguments`: [] let v<'T> : 'T list ref = ref [] -Он полностью соответствует своему названию: теперь вы не можете написать “v := [1]”, только “v := [1]”, и будет понятнее, что происходит на самом деле. -Если вы всё это уловили, я надеюсь, что у вас теперь есть чёткое понимание ограничения на значения в F#, и теперь вы можете при необходимости контролировать его с помощью явных аннотаций типов и аттрибута GeneralizableValue. Вместе с властью, однако, приходит и ответственность; статья на MSDN права - эти возможности редко используются в обыденном программировании на F#. В моём F# коде ти́повые функции появляются только в случаях, аналогичных lazy list - базовых структурах данных (ground cases of data structures); во всех остальных случаях я следую советам из статьи на MSDN: +Он полностью соответствует своему названию: теперь вы не можете написать `v := [1]`, только `v := [1]`, и будет понятнее, что происходит на самом деле. + +Если вы всё это уловили, я надеюсь, что у вас теперь есть чёткое понимание ограничений на значения в F#, и теперь вы можете при необходимости контролировать его с помощью явных аннотаций типов и аттрибута `GeneralizableValue`. Вместе с силой, однако, приходит и ответственность: как правильно сказано в статье на MSDN, эти возможности редко используются в повседневном программировании на F#. В моём F#-коде ти́повые функции появляются только в случаях, аналогичных `lazy list` - базовых структурах данных (ground cases of data structures); во всех остальных случаях я следую советам из статьи на MSDN: * Ограничьте тип так, чтобы он перестал быть полиморфным, добавив явную аннотацию типа к значению или параметру. * Если проблема заключается в использовании необобщаемой конструкции для определения полиморфной функции, такой как композиция функций или частичное применение аргументов каррированной функции, попробуйте переписать определение функции на обыкновенное. -* Если проблема заключается в том, что выражение слишком сложно для обобщения, превратите его в функцию, добавив неиспользуемый параметр. +* Если проблема заключается в том, что выражение слишком сложно для обобщения, превратите его в функцию, добавив неиспользуемый параметр. \ No newline at end of file