Pull to refresh

Comments 14

А где и для чего на практике это можно использовать?
«А где и для чего на практике это можно использовать?»
var x = await MySheduler.FirstTask.Start();

по сути идет формализация правил «вывода типов». Немножко матлогики и мы получаем все, что нужно.
Г ⊢ X: T означает, что «из гипотезы Г выводится то, что X принадлежит типу T» Значек ⊢ — это перевернутая «Т», от слова «Теорема». То есть существует теорема, доказывающее подобный вывод.
Автовывод из C# (var) и C++ (auto) не демонстрирует всего масштаба «трагедии». Пример на Haskell:
inums = [0..5]
fnums = [0..5]

main = do
     print inums
     let fprint = print :: [Double] -> IO ()
     fprint fnums

Вывод:
[0,1,2,3,4,5]
[0.0,1.0,2.0,3.0,4.0,5.0]

Несмотря на то, что именам inum и fnum были присвоены одинаковые выражения, компилятор вывел им разные типы, используя информацию в контексте их использования. Ни C#, ни C++, ни Scala такого не могут. Одна из впечатляющих демонстраций мощи системы типов в Haskell — модуль регулярных выражений, в котором один оператор =~ может возвращать разные типы значений в зависимости от контекста использования, и это всё преспокойно вписывается в систему типов (Regular expressions in Haskell). В RWH есть ещё впечатляющий пример с построением AST из арифметики: Extended example: Numeric Types.
var inums = Enumerable.Range(0, 6).ToList();
var fnums = Enumerable.Range(0, 6).ToList();
inums.ForEach(Console.WriteLine);
Action fprint = i => Console.WriteLine((double) i);
fnums.ForEach(fprint);

честно говоря не увидел, что намного мощнее. Лаконичнее — да, но это не во всем. Когда изучал функциональщину то тоже показалось, что вывод всего и вся, иммутабельность, сопоставление с образцом — все очень красиво и кратко. А когда решил реальную задачу попробовать смастерить — любимый шарп дал 100 очков форы. Конечно, можно сказать, что я плохо знаю функциональные языки, но результат ООП + лямбда-выражений зачастую оказывается не хуже, а иногда и лучше.

P.S. (почему-то теги так и не хотят работать… Видимо, репутация и на это влияет :( )
Между моим примером и вашим на самом деле есть принципиальная разница: в моём примере у выражений разный статический тип, а в вашем он одинаков, поэтому вам пришлось написать копию функции с рантайм-кастом. Это совсем не то.
Я не питаю иллюзий по поводу функциональщины, этап эйфории прошёл пару лет назад. Я видел реальный функциональный код, и мне часто сложно назвать его красивым и кратким. Мой текущий рабочий язык C++, а ради удовольствия я иногда пишу на Go, периодически балуясь функциональными языками. Но функциональные идеи очень сильно повлияли на меня и на мой стиль программирования. Это незаменимая школа дизайна.
Насчет школы и дизайна — абсолютно согласен. По крайней мере делегаты и локальные лямбды теперь используются намного чаще, а код — получается намного чище.

А вот насчет принципиальной разницы не особо понял. Может поясните более конкретно? Насколько я понял, у вас просто получается неявное преобразование Action к Action, этакая ковариация типа делегата, в шарпе это запрещено, то есть
пусть T: U, но тогда ⌐∀T (Generic(T): Generic(U)), то есть такую ковариацию можно произвести не для всех типов, а только для наследников, причем делегаты должны быть явно указанны как ковариационные. В шарпе, очевидно, int не является наследником double и наоборот тоже.

Либо я опять вас не понял)
неявное преобразование Action к Action

Нет, let в коде нужен только для того, чтобы создать контекст, в котором требуется [Double]. Вот другой пример:

inum = [0..5]
fnum = [0..5]

sins = map sin

printNums = do
          print inum
          print $ sins fnum


Загружаем в интерпретаторе:
Prelude> :load "HM.hs"
[1 of 1] Compiling Main             ( HM.hs, interpreted )
Ok, modules loaded: Main.
*Main> :t inum
inum :: [Integer]
*Main> :t fnum
fnum :: [Double]

Видите, компилятор вывел для inum и fnum разные статические типы. Т.е. не вставил каст из [Integer] в [Double], а именно изначално присвоил именам правильный статический тип. Он считает, что inum — это список целых, а fnum — список чисел с плавающей точкой, не смотря на то, что им присвоено одно и тоже выражение. У вас же и inum и fnum имеют один и тот же статический тип, компилятор c# считает их оба чем-то вроде IEnumerable<int>.
Посмотрите ниже. Для языков типа шарпа недостаточно знать, что это «целое» или «с плавающей запятой» Это может быть short, может быть byte, может быть знаковый или беззнаковый… Много чего может быть. И в общем случае задача вывода нетривиальная. Тут это наверняка как-то по-хитрому решается (сложнее, чем в примере выше, потому что там все-таки не охватывается все многообразие имо), но тут у нас остается какая-то степень контроля над программой (множество возможных значений числа, занимаемый объем памяти, и т.д.). В хаскелле афайк вообще длинная арифметика реализована неявно, а вот в том же шарпе всему этому приходится уделять внимание, я бы не сказал, что это сделано по недосмотру или небрежности.
Вы так говорите, будто в Haskell нет аналогов short и byte. Для длинной арифметики в Haskell есть тип Integer (используемый по умолчанию), есть и обычный Int. Типы Integer и Double выбраны исключительно в целях демонстрации.

По шагам логика компиляторов:
C#: о, переменной неуказанного типа присвоили значение типа X. Значит, её типом будет X.
Haskell: Ага, эту переменную инициализируют списком числовых констант. А ещё её используют в контексте, где требуется список чисел с плавающей точкой. Хм, чтобы типы в модуле сходились, тип переменной должен быть [Double], т.к. [Double] можно проинициализировать числовыми константами и передать в соответствующую функцию.

В реальности условия, рассматриваемые для вычисления оптимального типа, могут быть значительно более сложными и их может быть больше. Например, Haskell без труда вычисляет тип рекурсивных функций.
Круто. Даже не представляю, каким адским матаном это все реализовывалось. Вернее, матлогикой. Вернее даже представляю. Но просыпаться в холодном поту не хочу. Так что поверю вам на слово.
делегаты и локальные лямбды

Я имел в виду несколько большее, чем сахар лямбд.

Ленивые вычисления, монады (например, в контексте асинхронных и ленивых вычислений), функции высшего порядка и комбинаторы (один из моих любимых приёмов; IEnumerator можно считать примером программирования с помощью комбинаторов), классы типов (typeclasses), разделение «чистых» функций и функций с побочными эффектами, недетерминированные вычисления… Несчётное множество приёмов, которые можно применить независимо от языка, на котором пишешь.
Почти все это реализовано в шарпе, только местами немного кривокосо (напиример, «чистые» функции отличаются от обычных только добавлением аттрибута [Pure]).

Так что надеемся на развитие.

«Я имел в виду несколько большее, чем сахар лямбд.»
Ну так и я не про то, чтобы писать на 2 строчки меньше кода, а про то, что у меня иногда встречаются функции, которые принимают параметрами функции, которые работают с функциями. Не скажу, что это часто нужно, обычно это все же излишне, но например как в задаче многомерной нелинейной оптимизации, где целевая функция у нас в свою очередь имеет аргументом другие функции, это является насущной необходимостью
Хиндли-Милнер это один из самых популярных механизмов вывода типов в языках, поддерживающих функциональное программирование. Понять его на интуитивном уровне гораздо проще, чем кажется: находим использования символа в исходном коде и решаем систему уравнений в типах, используя самый общий тип в качестве решения. Он используется в компиляторах ML, OCaml и Haskell. Он позволяет практически не писать тэги типов, и, тем не менее, гарантировать типобезопасность. Scala, например, ради ООП пошла своим путём в выводе типов, который, на мой взгляд, гораздо менее прозрачен, чем в Haskell.
Sign up to leave a comment.

Articles