Има ли смисъл пъзелът с продълженията ин ян на въведен език?


person Andrej Bauer    schedule 12.02.2012    source източник


Отговори (3)


Мисля, че сам ще отговоря на въпроса си. Ще покажа две решения, едно в eff и друго в Ocaml.

еф

Ще работим с eff (тук надувам собствения си рог, вижте по-долу за друг начин в OCaml с Разширението delimcc на Олег.) Решението е обяснено в документа Програмиране с алгебрични ефекти и продължения.

Първо дефинираме shift и reset в eff:

type ('a, 'b) delimited =
effect
  operation shift : (('a -> 'b) -> 'b) -> 'a
end

let rec reset d = handler
  | d#shift f k -> with reset d handle (f k) ;;

Ето пъзела ин ян, транскрибиран в eff:

let y = new delimited in
  with reset y handle
    let yin = (fun k -> std#write "@" ; k) (y#shift (fun k -> k k)) in
    let yang = (fun k -> std#write "*" ; k) (y#shift (fun k -> k k)) in
      yin yang

Но eff се оплаква от това, че не може да реши уравнението на типа α = α → β. В момента eff не може да обработва произволни рекурсивни типове, така че сме блокирани. Като начин за измама можем да изключим проверката на типа, за да видим дали най-малкото кодът прави това, което трябва:

$ eff --no-types -l yinyang.eff
@*@**@***@****@*****@******@*******@********@*********@*******...

Добре, прави правилното нещо, но типовете не са достатъчно мощни.

OCaml

За този пример се нуждаем от библиотеката delimcc на Олег Кисельов. Кодът е както следва:

open Delimcc ;;

let y = new_prompt () in
  push_prompt y (fun () ->
    let yin = (fun k -> print_string "@" ; k) (shift y (fun k -> k k)) in
    let yang = (fun k -> print_string "*" ; k) (shift y (fun k -> k k)) in
      yin yang)

Отново Ocaml няма да компилира, защото попада на уравнение от рекурсивен тип. Но с опцията -rectypes ние можем да компилираме:

ocamlc -rectypes -o yinyang delimcc.cma yinyang.ml

Работи според очакванията:

$ ./yinyang
@*@**@***@****@*****@******@*******@********@*********@...

OCaml изчислява, че типът на yin и yang е ('a -> 'a) as 'a, което е неговият начин да се каже "тип α такъв, че α = α → α". Това е именно типовата характеристика на нетипизираните λ-изчислителни модели. Ето го, пъзелът ин ян по същество използва функции на нетипизираното λ-изчисление.

person Andrej Bauer    schedule 22.03.2012
comment
Опитах това в SML, но се натъкнах на същото ограничение за рекурсивни (кръгови) типове. - person Josh Segall; 23.03.2012

Възможно е да се декларира рекурсивен функционален тип в C#, статично типизиран език:

delegate Continuation Continuation(Continuation continuation);

Това определение е еквивалентно на α : α → α на ML.

Сега можем да „преведем“ пъзела ин-ян на C#. Това наистина изисква трансформация за повикването/cc и трябва да направим трансформацията два пъти, защото има две от тях в нея, но резултатът все още изглежда много като оригинала и все още има yin(yang) извикване в него:

Continuation c1 = cc1 =>
{
    Continuation yin = new Continuation(arg => { Console.Write("@"); return arg; })(cc1);
    Continuation c2 = cc2 =>
    {
        Continuation yang = new Continuation(arg => { Console.Write("*"); return arg; })(cc2);
        return yin(yang);
    };
    return c2(c2);
};
c1(c1);

Сега е ясно, че променливата yang е само в локален обхват, така че всъщност можем да я оптимизираме:

Continuation c1 = cc1 =>
{
    Continuation yin = new Continuation(arg => { Console.Write("@"); return arg; })(cc1);
    Continuation c2 = cc2 => yin(new Continuation(arg => { Console.Write("*"); return arg; })(cc2));
    return c2(c2);
};
c1(c1);

Сега разбираме, че тези малки вградени функции наистина просто извеждат знак и не правят нищо, така че можем да ги разопаковаме:

Continuation c1 = cc1 =>
{
    Console.Write("@");
    Continuation yin = cc1;
    Continuation c2 = cc2 =>
    {
        Console.Write("*");
        return yin(cc2);
    };
    return c2(c2);
};
c1(c1);

Накрая става ясно, че променливата yin също е излишна (можем просто да използваме cc1). За да запазите обаче оригиналния дух, преименувайте cc1 на yin и cc2 на yang и ще си върнем любимия yin(yang):

Continuation c1 = yin =>
{
    Console.Write("@");
    Continuation c2 = yang =>
    {
        Console.Write("*");
        return yin(yang);
    };
    return c2(c2);
};
c1(c1);

Всички по-горе са една и съща програма, семантично. Мисля, че крайният резултат сам по себе си е фантастичен C# пъзел. Така че бих отговорил на въпроса ви, като кажа: да, очевидно има много смисъл дори на статично типизиран език :)

person Timwi    schedule 22.03.2012
comment
Това е много готино, но не сте използвали продължения, които са в основата на ин ян пъзела. Не ме разбирайте погрешно, това, което направихте, е страхотно, просто не е оригиналният пъзел. Вместо това сте използвали λ-изчислението, тъй като вашето решение може да бъде пренаписано с чисти функции. C# наистина няма първокласни продължения, нали? За съжаление не мога да напиша еквивалентния OCaml код тук. Трябва ли да го публикувам като отделен отговор? - person Andrej Bauer; 23.03.2012
comment
Мисля, че използва продължения. Той просто не използва call-with-current-continuation, което C# няма. Моята програма е това, което получавате след първата трансформация, която компилаторът би трябвало да направи на програма с call/cc. Само защото компилира далеч call/cc не означава, че компилира далеч продължения. - person Timwi; 24.03.2012
comment
вярно И все пак, докато оригиналът изглежда страшно и объркващо, този изглежда просто объркващо :-) - person Andrej Bauer; 25.03.2012
comment
@AndrejBauer: Вашият въпрос не беше за страх или объркване, а дали „пъзелът има смисъл на въведен език“. - person Timwi; 25.03.2012
comment
Тази дискусия е безсмислена, защото до голяма степен е въпрос на мнение дали сте променили пъзела твърде много. Във всеки случай отговорът е, че пъзелът има смисъл в език с достатъчно рекурсивни типове (но тогава всичко има смисъл в такъв език). - person Andrej Bauer; 26.03.2012
comment
Мисля, че терминът продължение е достатъчно специфичен, за да се твърди, че C# всъщност ги няма. Въпреки това, този отговор ясно показва, че система със статичен тип няма да пречи, тъй като типът на продължението е лесно дефинируем в C#. - person Roman Starkov; 05.04.2012

Вижте също моя отговор на как работи пъзелът ин ян, на който трябваше да намеря отговор, преди да може да отговори на този.

Това, че е "типизиран" език, само по себе си не прави разликата дали този пъзел е изразим в него (без значение колко неясен е терминът "типизиран език"). Въпреки това, за да отговоря на въпроса ви най-буквално: да, възможно е, защото самият Scheme е типизиран език: всяка стойност има известен тип. Това очевидно не е това, което имахте предвид, така че предполагам, че имате предвид дали това е възможно на език, където на всяка променлива е присвоен постоянен тип, който никога не се променя (известен още като "статично въведен език").

Освен това ще предположа, че искате духът на пъзела да бъде запазен, когато е изразен на някакъв език. Очевидно е възможно да се напише интерпретатор на Scheme в машинен код x86 и очевидно е възможно да се напише интерпретатор на машинен код x86 на типизиран език, който има само цели типове данни и функционални указатели. Но резултатът не е в същия "дух". Така че, за да направя това по-точно, ще поставя допълнително изискване: резултатът трябва да бъде изразен с помощта на верни продължения. Не емулация, а истински пълни продължения.

И така, можете ли да имате статично въведен език с продължения? Оказва се, че можете, но все пак можете да го наречете измама. Например, в C#, ако моите продължения бяха определени като "функция, която взема обект и връща обект", където "обект" е тип, който може да съдържа всичко, ще намерите ли това за приемливо? Какво ще стане, ако функцията вземе и върне "динамика"? Какво ще стане, ако имам "типизиран" език, където всяка функция има един и същ статичен тип: "функция", без да дефинирам типовете аргументи и връщаните типове? Получената програма все още ли е в същия дух, въпреки че използва истински продължения?

Искам да кажа, че свойството "статично въведено" все още позволява огромно количество вариации в системата от типове, достатъчно, за да направи цялата разлика. Така че просто за забавление, нека помислим какво трябва да поддържа системата за типове, за да се квалифицира като неизмамна по каквато и да е мярка.

Операторът call/cc(x) може да бъде написан и като x(get/cc), което според мен е много по-лесно за разбиране. Тук x е функция, която приема продължение и връща стойност, докато get/cc връща Continuation. Continuation има всички черти на функция; може да бъде извикан с един аргумент и някак ще замести стойността, предадена където get/cc, който го е създал, е бил първоначално разположен, като допълнително ще възобнови изпълнението в този момент.

Това означава, че get/cc има неудобен тип: това е function, но същото местоположение в крайна сметка ще върне стойност, чийто тип все още не знаем. Да предположим обаче, че в духа на статично типизираните езици изискваме връщаният тип да бъде фиксиран. Тоест, когато извиквате обекта за продължение, можете да предавате само стойности от предварително дефиниран тип. С този подход типът на функцията за продължение може да бъде дефиниран с рекурсивния израз на формата T = function T->T. Както беше посочено от приятел, този тип всъщност може да бъде деклариран в C#: public delegate T T(T t);!

И така, ето го; това, че сте "въведени", не изключва, нито гарантира, че можете да изразите този пъзел, без да променяте неговия характер. Въпреки това, ако разрешите статичния тип "може да бъде всичко" (известен като object в Java и C#), тогава единственото друго нещо, от което се нуждаете, е поддръжка за истински продължения и пъзелът може да бъде представен без проблем.


Подхождайки към същия въпрос от различна гледна точка, помислете за моето пренаписване на пъзела в нещо, което повече напомня на традиционен статично въведен императивен език, който обясних в свързан отговор:

yin = (function(arg) { print @; return arg; })(get-cc);
yang = (function(arg) { print *; return arg; })(get-cc);
yin(yang);

Тук типът на yin и yang никога не се променя. Те винаги съхраняват "продължение C, което приема C и връща C". Това е много съвместимо със статичното писане, чието единствено изискване е типът да не се променя следващия път, когато изпълните този код.

person Roman Starkov    schedule 22.03.2012
comment
Благодаря за подробния отговор, но всъщност не отговаряте на въпроса ми. Изрично попитах за статичното писане a la ML. Мислех, че съм бил достатъчно ясен относно това във въпроса си, но може би не. Javascript всъщност не добавя нищо към отговора, защото точно като схемата не е статично въведен. Преведено на по-малко подробен, но по-прецизен език, вашият отговор е, че се нуждаем от рекурсивния тип α = α → α. Това е интересно, тъй като eff ми казва, че въпросното уравнение е α = α → β. Трябва ли α = β? - person Andrej Bauer; 22.03.2012
comment
@AndrejBauer Първо, изрично е определено невярно :) И второ, аз аз говоря за статично въведен език; Аз не говоря за JavaScript. Споменах JS синтаксис веднъж; Ще премахна това. - person Roman Starkov; 22.03.2012
comment
Какво би било по-ясно от това, че се опитвам да напиша еквивалентен код на въведен език, като SML/NJ, но ми дава грешки при въвеждане? Предполагам, че думата статичен липсва. Добре, ще го добавя само за да изглеждате зле ;-) Както и да е, не е важно. - person Andrej Bauer; 22.03.2012
comment
@AndrejBauer В моя защита, никога не съм чувал за този език и грешки при въвеждане могат да възникнат и по време на изпълнение :) Както и да е, радвам се, че получихте отговора си. - person Roman Starkov; 22.03.2012
comment
Правилно сте разбрали типа, това е α = α → α, а не α = α → β, както се съобщава от грешка при въвеждане в eff. OCaml може да изчисли типовете, вижте моя актуализиран отговор. Освен това правилното заключение е, че пъзелът ин ян е по същество нетипизиран в смисъл, че типовете yin и yang са същите като типовете, необходими за въвеждане на типизиране на нетипизираното λ-изчисление. - person Andrej Bauer; 22.03.2012
comment
@AndrejBauer Предполагам, че това е въпрос на дефиниции на думи. Докато мислим едно и също нещо (това е нещо, което взема подобен на себе си и връща подобен на себе си), тогава няма значение кои думи използвате, за да го опишете... - person Roman Starkov; 22.03.2012
comment
Мисля, че казваме повече или по-малко едно и също нещо, с изключение на това, че използвате неофициална терминология, докато аз имам предвид точни математически концепции (рекурсивни типове). - person Andrej Bauer; 22.03.2012