Есть две функции, которые могут где-то сохранять свое состояние и передавать друг другу управление напрямую, без вызова функции. Оператор yield делает нечто, что сохраняет состояние первой функции, переключается на вторую и восстанавливает состояние второй. Желательно, чтобы это нечто было достаточно низкоуровневым и эффективным, с тем чтобы данный оператор можно было применять где угодно, в том числе при программировании embedded софта и т.п. То есть никаких фреймворков нет, есть как-бы "голый си" (некий разрабатываемый си-подобный язык).
Интересно, как можно такую фичу грамотно спроектировать при разработке собственного языка программирования?
Какие требования, ограничения к сопрограммам должны предъявляться? Любые ли две функции можно сделать сопрограммами, или только удовлетворяющие какому-то общему требованию?
Здравствуйте, x-code, Вы писали: XC>Интересно, как можно такую фичу грамотно спроектировать при разработке собственного языка программирования?
Посмотреть, как это уже сделано в других языках? (Lisp, Python, генераторы в C#)?
XC>Интересно, как можно такую фичу грамотно спроектировать при разработке собственного языка программирования?
Реализуется такая фича, как сохранение текущего контекста выполнения, при этом сохраняются локальные переменные и стек вызовов. В простом случае, когда yield не может быть внутри вложенных функции, сохраняются только локальные переменные, так, например, сделан yield в C#.
Основная сложность — сделать так, чтобы поведение сопрограмм было предсказуемым: чтобы сопрограмма при выполнении вместе с другой сопрограммой выдавала тот же результат, что и при одиночном выполнении. Для этого лучше всего подходит функциональный подход, в чистом или смешанном виде.
Простой вариант компиляции сопрограмм:
1. тело функции разбивается на куски код между yield-ами и операциями перехода
2. локальные переменные преобразуются в поля объекта
3. добавляется переменная, которая определяется какой кусок кода должен выполняться следующим
тогда следующий код (на некотором вымышленном языке)
int F(int count)
{
var sum = 0;
for (var i = 0; i < count; ++i)
{
sum += i;
yield;
}
return sum;
}
при компиляции преобразуется в:
class F_Context
{
public F_Context(int count) {this.count = count;}
public int sum = 0;
public int count = 0;
public int i = 0;
public int result;
public int state = 0;
}
void F(F_Context context)
{
switch (context.state)
{
case 0:
context.sum = 0;
context.i = 0;
context.state = 1;
goto 1;
case 1:
if (context.i < context.count)
goto 3;
context.sum += context.i;
context.state = 2;
return;
case 2:
context.i = context.i + 1;
context.state = 1;
goto 1;
case 3:
context.result = context.sum;
context.state = -1;
return;
}
}
Здравствуйте, x-code, Вы писали:
XC>На википедии есть некий пример сопрограмм.
...
XC>Интересно, как можно такую фичу грамотно спроектировать при разработке собственного языка программирования?
С точки зрения языка тут особо нечего проектировать. Мне бы больше всего понравились обычные функции-интринсики.
А! Вспомнил! Хорошо бы было в типе функции как-то специфицировать, что она может прерываться/восстанавливаться. Это и для читабельности кода полезно, и компилятору может поможет.
Здесь ИМХО больше сложностей будет с точки зрения разработки компилятора. Надо будет чётко определить, что нужно сохранять при переключении между функциями и как это аккуратно восстановить потом (и как при этом не помешать оптимизации кода).
Помедитируй над ucontext.h и longjmp()/setjmp() — народ их пользует, но с опаской.
XC>Какие требования, ограничения к сопрограммам должны предъявляться? Любые ли две функции можно сделать сопрограммами, или только удовлетворяющие какому-то общему требованию?
Точно не скажу, но стоит обратить внимание на рекурентность, размер стэка (его же сохранять надо будет), всякие вызовы alloca().
XC>В общем, любые мысли приветствуются.
Сначала подумайте во что это компилироваться будет, как дебаггер будет работать, как в крэш-дампах вызовы корутин отображать.
Здравствуйте, x-code, Вы писали:
XC>На википедии есть некий пример сопрограмм. Если привести его в более сиподобный вид, то получится примерно следующее:
XC>В общем, любые мысли приветствуются.
Не очень понятно, какие конкретно проблемы предлагается решить при помощи сопрограмм.
Я понимаю, скажем, шарповые генераторы — автоматическое переписывание энергичного кода в ленивый, что удобно, когда код легче писать в энергичном стиле.
При этом продюсер остаётся полностью развязанным с консумером. В примере, где они жёстко связаны, я вообще не вижу смысла их разделять — вместо yield consume просто вставляем тот кусок кода, который был в консумере, и всё.
Было бы здорово иметь ещё и обратную операцию — т.е. когда энергичный потребляющий код автоматически переписывается в ленивый.
То есть из вот такого:
public int Sum(IEnumerable<int> input)
{
int sum = 0;
foreach(item in input)
sum += item;
return sum;
}
мы делаем что-то вроде
public class Sum
{
int _sum;
public Sum() { _sum=0;}
public void Input(int item, bool end)
{
_sum += item;
}
public int Result { return _sum;}
}
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Здравствуйте, Sinclair, Вы писали:
S>Не очень понятно, какие конкретно проблемы предлагается решить при помощи сопрограмм.
асинхронный код.
например, async/await в .NET
Здравствуйте, x-code, Вы писали:
XC>Интересно, как можно такую фичу грамотно спроектировать при разработке собственного языка программирования? XC>Какие требования, ограничения к сопрограммам должны предъявляться? Любые ли две функции можно сделать сопрограммами, или только удовлетворяющие какому-то общему требованию?
yield реализуется сохранением стека в переменной ассоциированной с function instance.
XC>В общем, любые мысли приветствуются.
А вообще если в языке поддерживаются inner functions то coroutines делаются/эмулируются достаточно легко и так.
Скажем вот генератор который выдает элементы массива на JavaScript:
function nothing() {}
function generator(arr)
{
var i = -1;
return function() { return (++i < arr.length)? arr[i]:nothing; }
}
// используемvar arr = [0,1,2,3,4,5];
var gen = generator(arr);
while((el = gen()) !== nothing) {
...
}
Очевидно что generator() ведет себя в точности как coroutine сохраняя состояние исполнения между вызовами.
S>public int Sum(IEnumerable<int> input)
S>{
S> int sum = 0;
S> foreach(item in input)
S> sum += item;
S> return sum;
S>}
S>
S>мы делаем что-то вроде S>
S>public class Sum
S>{
S> int _sum;
S> public Sum() { _sum=0;}
S> public void Input(int item, bool end)
S> {
S> _sum += item;
S> }
S> public int Result { return _sum;}
S>}
S>
Это уже суперкомпиляция какая-то получается, осилить ее очень трудно.
Здравствуйте, x-code, Вы писали:
XC>Есть две функции, которые могут где-то сохранять свое состояние и передавать друг другу управление напрямую, без вызова функции. Оператор yield делает нечто, что сохраняет состояние первой функции, переключается на вторую и восстанавливает состояние второй. Желательно, чтобы это нечто было достаточно низкоуровневым и эффективным, с тем чтобы данный оператор можно было применять где угодно, в том числе при программировании embedded софта и т.п. То есть никаких фреймворков нет, есть как-бы "голый си" (некий разрабатываемый си-подобный язык).
XC>Интересно, как можно такую фичу грамотно спроектировать при разработке собственного языка программирования? XC>Какие требования, ограничения к сопрограммам должны предъявляться? Любые ли две функции можно сделать сопрограммами, или только удовлетворяющие какому-то общему требованию?
XC>В общем, любые мысли приветствуются.
Лучшая низкоуровневая реализация сопрограмм (правда, только для x86) которую я видел (libconcurrent), написана каким-то японцем. В принципе, глядя на нее можно многое понять, тем более, что там нет завязок на setjump/longjump и setcontext/getcontext. Работает достаточно быстро.
Некоторые ограничения на сопрограммы довольно очевидны — например, не стоит включать в контекст указатели на переменные стека. Могут возникнуть какие-то сложности с многопоточными программами (т.е. надо не запутаться между стеком thread-а и стектом coroutine). Еще может возникнуть противоречие между механизмом исключений и сопрограммами, т.к. они используют похожие методы. В общем — проблем хватает.
Теоретический аспект Coroutine хорошо описан Роберто нашим, Иерусалимским вот в этой статье.
Здравствуйте, VladD2, Вы писали:
VD>Здравствуйте, hardcase, Вы писали:
H>>Кто-то осислил специализатор для CIL.
VD>А можно в двух словах о том, что эта штука делает?
А я тебе полгода назад давал ссылку на пдф-ку по этому Cilpe.
H>Это уже суперкомпиляция какая-то получается, осилить ее очень трудно.
Никакого отношения к суперкомпиляции это не имеет. Трансформация в целом аналогична трансформациям для yield return.
Почитайте про Rx и дуализм енумераторов и событий.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Здравствуйте, Sinclair, Вы писали:
S>Здравствуйте, hardcase, Вы писали:
H>>Это уже суперкомпиляция какая-то получается, осилить ее очень трудно. S>Никакого отношения к суперкомпиляции это не имеет. Трансформация в целом аналогична трансформациям для yield return. S>Почитайте про Rx и дуализм енумераторов и событий.
Все верно Я невнимательно посмотрел на код. Действительно, операция "выворачивания на изнанку" кода примера в целом аналогична переписыванию в yield-автомат. Сделать аналог в Nemerle в принципе реально.
Я недавно реализовал coroutines в своём DSL.
В общем-то всё просто. Переключение контекстов.
Coroutines организуют дерево.
Главное ограничение — один объект coroutine может быть в дереве один раз.
Это не значит, что одна функция может быть один раз. Просто для каждого вхождения нужен свой контекст.
func f()
for i in 1..100
yield i
end
end
for i in f
print(i)
end
Тут for обнаруживает, что f это функция, создаёт coroutine — отдельный контекст исполнения с собственным стэком и точкой входа f в качестве текущей инструкции, и переключает исполнение на неё.
Родителем этой coroutine будет основной контекст, в точкой возврата специальный код отслеживающий закончилась ли coroutine или вернула значение.
Ну и yield переключает контекст в родительский, и возвращает значение.
Можно делать так:
func g()
for i in 1..100
yield i
end
end
func f()
for i in g
yield i
end
end
for i in f
print(i)
end
без проблем.
Вот пример linq подобного запроса на coroutine:
class Query(src)
src
func Select(sfunc)
srcVal=src
src=func()
for val in srcVal
yield sfunc(val)
end
end
return self
end
func Where(wfunc)
srcVal=src
src=func()
for val in srcVal
if wfunc(val)
yield val
end
end
end
return self
end
func SelectMany(csfunc,sfunc)
srcVal=src
src=func()
for val in srcVal
for val2 in csfunc(val)
yield sfunc(val,val2)
end
end
end
return self
end
func get()
return src
end
end
class PetOwner(name,pets)
name
pets
end
a=[
PetOwner("Higa",["Scruffy", "Sam"])
PetOwner("Ashkenazi",["Walker", "Sugar"])
PetOwner("Price",["Scratches", "Diesel"])
PetOwner("Hines",["Dusty"])
]
q=Query(a).
SelectMany(!\1.pets,!{owner=>\1,pet=>\2}).
Where(!\1->pet[0]=="S").
Select(!{oname=>\1->owner.name,pname=>\1->pet}).get()
for i in q
print("$i->oname:$i->pname")
end
здесь evaluation будет происходить лениво, каждый этап в своей coroutine.