Программирование в функциональном стиле на C#
От: Эйнсток Файр Мухосранск Странный реагент
Дата: 15.10.22 04:53
Оценка:
Возможно ли программировать в функциональном стиле на C# ?
Примерно так, как программировали в объектно-ориентированном стиле на Си (например в библиотеке GObject).

Методика такая:
все понятия описываем интерфейсами (потому что есть множественное наследование интерфейсов),
а весь код пишем в функциональном стиле.

Это уже можно назвать передовой объектно-функциональной методологией или ещё нет?

Раньше ведь как было — осуждали глобальные переменные, и этого вроде хватало.
Что поменялось? Теперь осуждаем переменные класса? Ну ок.

Высказывал ли кто такую идею в виде книги?
Отредактировано 15.10.2022 5:10 Эйнсток Файр . Предыдущая версия . Еще …
Отредактировано 15.10.2022 5:09 Эйнсток Файр . Предыдущая версия .
c# функциональное программирование методологии
Re: Программирование в функциональном стиле на C#
От: Эйнсток Файр Мухосранск Странный реагент
Дата: 15.10.22 05:08
Оценка:
https://hamidmosalla.com/2019/04/25/functional-programming-in-c-sharp-a-brief-guide/
Re: Программирование в функциональном стиле на C#
От: Sinclair Россия https://github.com/evilguest/
Дата: 15.10.22 06:22
Оценка: 4 (1)
Здравствуйте, Эйнсток Файр, Вы писали:

ЭФ>Возможно ли программировать в функциональном стиле на C# ?

Да. Наличие лямбд и паттерн матчинга позволяет много что написать в функциональном стиле.
ЭФ>Примерно так, как программировали в объектно-ориентированном стиле на Си (например в библиотеке GObject).

ЭФ>все понятия описываем интерфейсами (потому что есть множественное наследование интерфейсов),

ЭФ>а весь код пишем в функциональном стиле.
Функциональный стиль с интерфейсами никак не связан (ну, кроме IEnumerable / IDictionary) Скорее я бы говорил о структурных типах и неизменяемости.

В функциональнсм стиле код и данные разделены. При этом для того, чтобы выполнить некоторый код над некоторыми данными, нам не нужно, чтобы эти "данные" реализовывали какой-то интерфейс.
Грубо говоря, когда мы пытаемся превратить коллекцию экземпляров некоторого типа T в строку-через-запятую, то в ООП мы полагаемся на наличие метода T.ToString(). Либо он наследует некоторому базовому типу с таким методом, либо реализует какой-то интерфейс. Вот как мы победили сырость.
А в ФП мы умеем преобразовывать в строку-через-запятую только коллекции строк.
Для всего остального есть функция map:
join(map(myobjects, o=> o.DisplayName), ", ")

Здесь нам совершенно неважно, реализуют ли объекты в myobjects хоть какой-то интерфейс. Приведением их к строке занимается не интерфейс, реализация которого гвоздями прибита к типу данных, а маленькая лямбда, написанная для конкретного случая.

ЭФ>Это уже можно назвать передовой объектно-функциональной методологией или ещё нет?

Вряд ли.
ЭФ>Раньше ведь как было — осуждали глобальные переменные, и этого вроде хватало.
ЭФ>Что поменялось? Теперь осуждаем переменные класса? Ну ок.
Поменялось представление о том, как должен быть устроен код.

Можно для интересу сравнить, как выглядит код околокомпиляторных задачек на ООП (визитор, визитор, визитор) и на ФП (рекурсия с паттерн-матчингом).
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[2]: Программирование в функциональном стиле на C#
От: samius Япония http://sams-tricks.blogspot.com
Дата: 15.10.22 12:37
Оценка:
Здравствуйте, Sinclair, Вы писали:

S>Здравствуйте, Эйнсток Файр, Вы писали:


ЭФ>>Возможно ли программировать в функциональном стиле на C# ?

S>Да. Наличие лямбд и паттерн матчинга позволяет много что написать в функциональном стиле.

Позволю сеюе немного не согласиться. Точнее, чуть расставить акценты.
Лямбды и ПМ — сахар. Мы можем дешугарнуть любой код, написанный в функциональном стиле и обнаружить, что он все еще в функциональном стиле. То есть, формально, не наличие лямбд и ПМ делают позволяют написать код в функциональном стиле. Написать код без них в функциональном стиле возможно. В смысле возможности достижения результата. Но сам процесс написания кода в функциональном стиле без лямбд и ПМ будет затруднен практически настолько, что лучше и не начинать.

К тем фичам, которые позволяют много что написать (в практическом смысле) можно добавить вывод типов, туплы.

Очень не хватает размеченных объединений, концептов, или чего-то вроде computation expressions (F#), оптимизации хвостовой рекурсии (гарантированной языком, а не джитом и фазой луны).
Re[3]: Программирование в функциональном стиле на C#
От: vaa  
Дата: 17.10.22 01:34
Оценка:
Здравствуйте, samius, Вы писали:

S>Очень не хватает размеченных объединений, концептов, или чего-то вроде computation expressions (F#), оптимизации хвостовой рекурсии (гарантированной языком, а не джитом и фазой луны).


Точно ли монады типа computation expressions являются атрибутом ФП?
По-моему монады это побочный продукт, типа паттернов из ООП.
Мне кажется ФП это прежде всего построение решения на чистых функциях.
☭ ✊ В мире нет ничего, кроме движущейся материи.
Re[4]: Программирование в функциональном стиле на C#
От: samius Япония http://sams-tricks.blogspot.com
Дата: 17.10.22 03:17
Оценка: 10 (2)
Здравствуйте, vaa, Вы писали:

vaa>Здравствуйте, samius, Вы писали:


S>>Очень не хватает размеченных объединений, концептов, или чего-то вроде computation expressions (F#), оптимизации хвостовой рекурсии (гарантированной языком, а не джитом и фазой луны).


vaa>Точно ли монады типа computation expressions являются атрибутом ФП?

тут надо поковыряться. Связь монад с computation expression примерно та же, что и связь монад в C# с Query Expression синтаксисом через стандартные названия SelectMany. Если мы SelectMany переименуем в Bind, то монада останется монадой, но мы не сможем с ней работать через query expression.
Так и с computation expression, это лишь надстройка для того, что бы монаду можно было засахаривать.

Теперь о том, являются ли монады атрибутом ФП? Да, конечно. Но это не неотъемлемый атрибут. Примерно, как если открутить от велосипеда сидение. Он останется велосипедом, но езда на нем не обещает быть комфортной.
vaa>По-моему монады это побочный продукт, типа паттернов из ООП.
Соглашусь. Их вполне можно считать паттерном ФП.
vaa>Мне кажется ФП это прежде всего построение решения на чистых функциях.
Если даже решения на чистых функциях назвать чистым ФП, а решения на нечистых функциях назвать нечистым ФП, то монады тут вряд-ли что-то меняют. Мы ведь можем завернуть в монаду как чистую фукнцию, так и нечистую. Оставим это на совести программиста. В этом аспекте возможность засахаривания монады — она перпендикулярна чистоте функций, завернутых в монаду.

Однако, все популярные монады строятся на чистых функциях. Identity, Maybe, Either, List, State, Continuation. И даже IO monad в Haskell формально чистая в том числе потому, что оперирует функциями, которые не вызывает в момент трансформации.
Re: Программирование в функциональном стиле на C#
От: vaa  
Дата: 17.10.22 12:48
Оценка:
Здравствуйте, Эйнсток Файр, Вы писали:

ЭФ>Возможно ли программировать в функциональном стиле на C# ?

ЭФ>Примерно так, как программировали в объектно-ориентированном стиле на Си (например в библиотеке GObject).

ЭФ>Методика такая:

ЭФ>все понятия описываем интерфейсами (потому что есть множественное наследование интерфейсов),
ЭФ>а весь код пишем в функциональном стиле.

ЭФ>Это уже можно назвать передовой объектно-функциональной методологией или ещё нет?


ЭФ>Раньше ведь как было — осуждали глобальные переменные, и этого вроде хватало.

ЭФ>Что поменялось? Теперь осуждаем переменные класса? Ну ок.

На самом деле интерфейс нужен только один
T -> U

и это есть...
    Func<T,U> func;

Применяя ее в различных вариантах получаем ФП.
Но заметьте, что ФП глубоко плевать на символы, т.е. не важно как называется функция, гораздо важнее для ФП ее сигнатура.
В классическом же интерфейсе все придают значение смыслу названий его элементов.
☭ ✊ В мире нет ничего, кроме движущейся материи.
Re[3]: Программирование в функциональном стиле на C#
От: Sinclair Россия https://github.com/evilguest/
Дата: 23.10.22 14:44
Оценка: 6 (1)
Здравствуйте, samius, Вы писали:

S>Лямбды и ПМ — сахар. Мы можем дешугарнуть любой код, написанный в функциональном стиле и обнаружить, что он все еще в функциональном стиле.

Дешугарнутые лямбды — не сахар (pun intended). В классическом ФП без них просто невозможно работать — ведь там нет стейта как такового.
В шарпе мы просто получаем катастрофически многословный код, в котором увидеть функциональный стиль уже довольно-таки тяжело:
public class AgeGreaterThanOrEqualFilter
{
   private int _age;
   public AgeGreaterThanOrEqualFilter(int age) => _age = age;
   public bool Predicate(Person p) => p.Age >= age;
}
...
var adults = people.Filter(new AgeGreaterThanOrEqualFilter(minAge).Predicate)

Этот "дешугарнутый" код гораздо ближе к чистому ООП, чем к ФП.
Вот оно, чистое ООП:
interface IFunction<T, R> 
{
   R Invoke(T arg);
}
interface IPredicate<T> : IFunction<T, bool>{}

class AgeGreaterThanOrEqualFilter: IPredicate<Person>
{
   private int _age;
   public AgeGreaterThanOrEqualFilter(int age) => _age = age;
   public bool Invoke(Person p) => p.Age >= age;
}
...
var adults = people.Filter(new AgeGreaterThanOrEqualFilter(18))

Классический паттерн "стратегия". Отличия от "дешугарнутой лямбды" минимальны.

А вот, чисто для сравниения, функциональный стиль:
var adults = people.Filter(p=>p.Age>=minAge);


S>То есть, формально, не наличие лямбд и ПМ делают позволяют написать код в функциональном стиле. Написать код без них в функциональном стиле возможно. В смысле возможности достижения результата. Но сам процесс написания кода в функциональном стиле без лямбд и ПМ будет затруднен практически настолько, что лучше и не начинать.

Всё же мне кажется, что без лямбд и ПМ от функционального стиля остаётся очень мало.
Ну, вот у нас же была функция qsort в stdlib. Вроде почти всё функционально. Но там в аргументе идёт stateless-функция; и средства порождать её на ходу нету.
Для сортировки такое нужно редко; а вот фильтрация с параметром, как в примере выше, вообще недостижима в рамках С без унизительных приседаний.
То есть мы сможем написать такой код, и он даже будет работать, но назвать это функциональным стилем — увольте.

Далее, про паттерн-матчинг. Тут как бы тоже, теоретически мы можем рассахарить его на цепочку if (a is UnaryExpression) { var a_ = a as UnaryExpression; var nt = a_.NodeType; var e = a_.Operand; ... } .
Но в реальности это настолько неудобно, что так никто не делает, и, в частности, обход гетерогенных деревьев делается через визиторы.
В итоге, скажем, код по вычислению значения простого арифметического выражения со сложениями и умножениями в целых числах в ООП подходе выглядит так:

abstract class Expression
{
   public abstract void Visit(ExpressionVisitor ev);
};

class IntConst: Expression
{
   public int Value {get;init;}
   public override void Visit(ExpressionVisitor ev) => ev.Visit(this);
}

class Add: Expression
{
   public Expression Left {get;init;}
   public Expression Right {get;init;}
   public override void Visit(ExpressionVisitor ev) => ev.Visit(this);
}
class Mul: Expression
{
   public Expression Left {get;init;}
   public Expression Right {get;init;}
   public override void Visit(ExpressionVisitor ev) => ev.Visit(this);
}
abstract class ExpressionVisitor
{
   public virtual void Visit(Expression e) => e.Visit(this);
   public virtual void Visit(IntConst ic) {};
   public virtual void Visit(Add a)
   {
      Visit(a.Left);
      Visit(a.Right);
   }
   public virtual void Visit(Mul m)
   {
      Visit(m.Left);
      Visit(m.Right);
   }
}

class CalculatingVisitor: ExpressionVisitor
{
   public int Value;
   override void Visit(Expression e)
   {
     Value = 0;
     base.Visit(e);
   }
   override void Visit(IntConst ic)
   {
      base.Visit(ic);
      Value = ic;
   }
   override void Visit(Add a)
   {
      var oldValue = Value;
      Visit(a.Left);
      var left = Value;
      Visit(a.Right);
      var right = Value;
      Value = left + right;
   }
   override void Visit(Mul a)
   {
      var oldValue = Value;
      Visit(a.Left);
      var left = Value;
      Visit(a.Right);
      var right = Value;
      Value = left * right;
   }
}


Давайте сравним его с функциональным стилем:
public int Calculate(Expression e)
  => e switch {
    IntConst(c) => c,
    Add(l, r) => Calculate(l) + Calculate(r),
    Mul(l, r) => Calculate(l) * Calculate(r)
  }

Мягко говоря, этот подход выглядит намного лучше. Если мы попробуем рассахарить ПМ, то получится не так плохо, как в чистом ООП, но всё ещё стрёмно:
public int Calculate(Expression e)
{ 
  if(e is IntConst)
  {
     var ic = e as IntConst;
     return ic.Value;
  } else if(e is Add)
  {
    var a = e as Add;
    return Calculate(a.Left) + Calculate(a.Right);
  }
  if(e is Mul)
  {
    var m = e as Mul;
    return Calculate(m.Left) + Calculate(m.Right);
  }

S>К тем фичам, которые позволяют много что написать (в практическом смысле) можно добавить вывод типов, туплы.
Вывод типов в C# очень базовый, до простейших ФП языков не дотягивает.
Вот так же функция Calculate на языке flow9:
calculate(ex)
{
  switch(ex)
  {
    IntConst(c): c;
    Add(l, r): calculate(l) + calculate(r);
    Mul(l, r): calculate(l) * calculate(r);
  }
}

Обратите внимание на то, что у функции не указан ни тип аргумента, ни тип результата. Тем не менее, оба успешно выводятся из использования. Причём прямо так полноценно выводятся — в частности, компилятор будет ругаться, если не все варианты Expression покрыты свитчем. В шарпе ничего подобного сделать не получится.

S>Очень не хватает размеченных объединений, концептов, или чего-то вроде computation expressions (F#), оптимизации хвостовой рекурсии (гарантированной языком, а не джитом и фазой луны).

Размеченные объединения плохо дружат с ООП и наследованием. Ограничить их только struct-типами? Многие сценарии станут неудобными.
Пока C# не собирается полностью отказываться от ООП, вместо размеченных объединений эффективнее использовать закрытые иерархии. Единственное чего там не хватает — анализа полноты switch. Но, быть может, он заработает и без специальной поддержки дискриминации?
Вместо концептов сейчас бурно развиваются статики в интерфейсах.
С хвостовой рекурсией всё действительно плохо; но я вообще не видел языков, в которых бы она была нормально сделана, кроме clojure.
Полагаю, что как раз потому, что непонятно, как её синтаксически записать. В кложе-то хорошо — там нет никакого синтаксиса, поэтому испортить его невозможно
А как в рамках шарпа написать "хочу хвостовой вызов", чтобы компилятор сразу фейлился если не получилось, и при этом вызов читался бы нормально — вот хз.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[4]: Программирование в функциональном стиле на C#
От: samius Япония http://sams-tricks.blogspot.com
Дата: 24.10.22 11:34
Оценка: 127 (2)
Здравствуйте, Sinclair, Вы писали:

S>Здравствуйте, samius, Вы писали:


S>>Лямбды и ПМ — сахар. Мы можем дешугарнуть любой код, написанный в функциональном стиле и обнаружить, что он все еще в функциональном стиле.

S>Дешугарнутые лямбды — не сахар (pun intended). В классическом ФП без них просто невозможно работать — ведь там нет стейта как такового.
Так а мы и не про стейт как раз. Если в классическом ФП нет стейта, то лямбда в классическом ФП стейт не заносит. В том же Haskell лямбда — это форма записи фукнции без имени. Анонимная функция. Вполне без них можно обходиться, используя функции с именем. Затруднения будут лишь в случаях, когда нужно записать какой-нибудь высокоуровневый комбинатор. Под стейтом я подразумеваю именно изменяемый стейт, т.е. программирование сайд-эффектов, а не то, что обычно прячется под монадой State, например.

S>В шарпе мы просто получаем катастрофически многословный код, в котором увидеть функциональный стиль уже довольно-таки тяжело:

S>
S>public class AgeGreaterThanOrEqualFilter
S>{
S>   private int _age;
S>   public AgeGreaterThanOrEqualFilter(int age) => _age = age;
S>   public bool Predicate(Person p) => p.Age >= age;
S>}
S>...
S>var adults = people.Filter(new AgeGreaterThanOrEqualFilter(minAge).Predicate)
S>

Тут надо договариваться. Когда мы говорим о функциональном стиле, то подразуммеваем сущностное (фактическое отсутствие сайд-эффектов), или красивое (насколько тяжело увидеть фактическое отсутствие сайд-эффектов за килотоннами дешугарнутого кода)??? Я подразумевал первое. Сайд-эффекты не создаются при рассахаривании.

S>Этот "дешугарнутый" код гораздо ближе к чистому ООП, чем к ФП.

S>Вот оно, чистое ООП:
S>
S>interface IFunction<T, R> 
S>{
S>   R Invoke(T arg);
S>}
S>interface IPredicate<T> : IFunction<T, bool>{}

S>class AgeGreaterThanOrEqualFilter: IPredicate<Person>
S>{
S>   private int _age;
S>   public AgeGreaterThanOrEqualFilter(int age) => _age = age;
S>   public bool Invoke(Person p) => p.Age >= age;
S>}
S>...
S>var adults = people.Filter(new AgeGreaterThanOrEqualFilter(18))
S>

S>Классический паттерн "стратегия". Отличия от "дешугарнутой лямбды" минимальны.
Я не соглашусь, что это близко к ООП. AgeGreaterThanOrEqualFilter не обладает поведением в смысле ООП. Ведет себя как чистая функция, а не как объект с состоянием.

S>А вот, чисто для сравниения, функциональный стиль:

S>
S>var adults = people.Filter(p=>p.Age>=minAge);
S>

Стиль в смысле "красиво" — да. А в смысле "что там под капотом" — это эквивалентный код.

S>>То есть, формально, не наличие лямбд и ПМ делают позволяют написать код в функциональном стиле. Написать код без них в функциональном стиле возможно. В смысле возможности достижения результата. Но сам процесс написания кода в функциональном стиле без лямбд и ПМ будет затруднен практически настолько, что лучше и не начинать.

S>Всё же мне кажется, что без лямбд и ПМ от функционального стиля остаётся очень мало.
S>Ну, вот у нас же была функция qsort в stdlib. Вроде почти всё функционально. Но там в аргументе идёт stateless-функция; и средства порождать её на ходу нету.
S>Для сортировки такое нужно редко; а вот фильтрация с параметром, как в примере выше, вообще недостижима в рамках С без унизительных приседаний.
S>То есть мы сможем написать такой код, и он даже будет работать, но назвать это функциональным стилем — увольте.
Опять таки, если мы используем термин "функциональный" как используем его при сравнении двух утюгов, то утюг с отпаривателем и селфклинингом будет более функционален.
Но при использовании термина "функциональный" в контексте фукнционального программирования как программирования без сайд-эффектов, сишная qsort не может быть фукнциональна даже на уровне "вроде почти всё функионально", т.к. она и есть чистый сайдэффект в отношении того, что она делает над входными даннымми. Да, одним из аргументов стейтлесс-функция, но другим — область памяти для грязи.

S>Далее, про паттерн-матчинг. Тут как бы тоже, теоретически мы можем рассахарить его на цепочку if (a is UnaryExpression) { var a_ = a as UnaryExpression; var nt = a_.NodeType; var e = a_.Operand; ... } .

S>Но в реальности это настолько неудобно, что так никто не делает, и, в частности, обход гетерогенных деревьев делается через визиторы.
S>В итоге, скажем, код по вычислению значения простого арифметического выражения со сложениями и умножениями в целых числах в ООП подходе выглядит так:

S>
S>abstract class Expression
S>{
S>   public abstract void Visit(ExpressionVisitor ev);
S>};

S>class IntConst: Expression
S>{
S>   public int Value {get;init;}
S>   public override void Visit(ExpressionVisitor ev) => ev.Visit(this);
S>}

S>class Add: Expression
S>{
S>   public Expression Left {get;init;}
S>   public Expression Right {get;init;}
S>   public override void Visit(ExpressionVisitor ev) => ev.Visit(this);
S>}
S>class Mul: Expression
S>{
S>   public Expression Left {get;init;}
S>   public Expression Right {get;init;}
S>   public override void Visit(ExpressionVisitor ev) => ev.Visit(this);
S>}
S>abstract class ExpressionVisitor
S>{
S>   public virtual void Visit(Expression e) => e.Visit(this);
S>   public virtual void Visit(IntConst ic) {};
S>   public virtual void Visit(Add a)
S>   {
S>      Visit(a.Left);
S>      Visit(a.Right);
S>   }
S>   public virtual void Visit(Mul m)
S>   {
S>      Visit(m.Left);
S>      Visit(m.Right);
S>   }
S>}

S>class CalculatingVisitor: ExpressionVisitor
S>{
S>   public int Value;
S>   override void Visit(Expression e)
S>   {
S>     Value = 0;
S>     base.Visit(e);
S>   }
S>   override void Visit(IntConst ic)
S>   {
S>      base.Visit(ic);
S>      Value = ic;
S>   }
S>   override void Visit(Add a)
S>   {
S>      var oldValue = Value;
S>      Visit(a.Left);
S>      var left = Value;
S>      Visit(a.Right);
S>      var right = Value;
S>      Value = left + right;
S>   }
S>   override void Visit(Mul a)
S>   {
S>      var oldValue = Value;
S>      Visit(a.Left);
S>      var left = Value;
S>      Visit(a.Right);
S>      var right = Value;
S>      Value = left * right;
S>   }
S>}
S>


Согласен, что это типичный ООП подход. Но то, что его делает ООП — это сайд эффекты, накопление изменений состояния внутри тел визитеров.
Стоит его записать в форме
T Visit(Expression)
вместо
void Visit(Expression)
и мы получим что-то близкое к набору стратегий, описанных правилами диспетчеризации, что выглядит вполне функционально в отношении сайд эффетов, но довольно неудобно по сравнению с использованием ПМ.

S>Давайте сравним его с функциональным стилем:

S>
S>public int Calculate(Expression e)
S>  => e switch {
S>    IntConst(c) => c,
S>    Add(l, r) => Calculate(l) + Calculate(r),
S>    Mul(l, r) => Calculate(l) * Calculate(r)
S>  }
S>

S>Мягко говоря, этот подход выглядит намного лучше. Если мы попробуем рассахарить ПМ, то получится не так плохо, как в чистом ООП, но всё ещё стрёмно:
S>
S>public int Calculate(Expression e)
S>{ 
S>  if(e is IntConst)
S>  {
S>     var ic = e as IntConst;
S>     return ic.Value;
S>  } else if(e is Add)
S>  {
S>    var a = e as Add;
S>    return Calculate(a.Left) + Calculate(a.Right);
S>  }
S>  if(e is Mul)
S>  {
S>    var m = e as Mul;
S>    return Calculate(m.Left) + Calculate(m.Right);
S>  }
S>

Согласен, стремно.
S>>К тем фичам, которые позволяют много что написать (в практическом смысле) можно добавить вывод типов, туплы.
S>Вывод типов в C# очень базовый, до простейших ФП языков не дотягивает.
S>Вот так же функция Calculate на языке flow9:
S>
S>calculate(ex)
S>{
S>  switch(ex)
S>  {
S>    IntConst(c): c;
S>    Add(l, r): calculate(l) + calculate(r);
S>    Mul(l, r): calculate(l) * calculate(r);
S>  }
S>}
S>

S>Обратите внимание на то, что у функции не указан ни тип аргумента, ни тип результата. Тем не менее, оба успешно выводятся из использования. Причём прямо так полноценно выводятся — в частности, компилятор будет ругаться, если не все варианты Expression покрыты свитчем. В шарпе ничего подобного сделать не получится.
Я согласен, что вывод типов не самый продвинутый, однако, цепочки generic методов с шаблонными параметрами и результатом — вполне удаются без спецификаций в значительном кол-ве случаев.

S>>Очень не хватает размеченных объединений, концептов, или чего-то вроде computation expressions (F#), оптимизации хвостовой рекурсии (гарантированной языком, а не джитом и фазой луны).

S>Размеченные объединения плохо дружат с ООП и наследованием. Ограничить их только struct-типами? Многие сценарии станут неудобными.
Решение из F# кажется вполне годным.

S>Пока C# не собирается полностью отказываться от ООП, вместо размеченных объединений эффективнее использовать закрытые иерархии. Единственное чего там не хватает — анализа полноты switch. Но, быть может, он заработает и без специальной поддержки дискриминации?

анализ полноты свитча, может быть, заработает. Хотелось бы просто короткого сособа объявления типа "это или то". Закрытую иерархию я в состоянии описать. Или даже сгенерировать ее описание. Но ведь можно было ранее и Record-ы описывать руками. А теперь для них есть решение, а для дискриминаций — нет.

S>Вместо концептов сейчас бурно развиваются статики в интерфейсах.

Наверное, это то, чего я жду. Надо поглядеть, спасибо.

S>С хвостовой рекурсией всё действительно плохо; но я вообще не видел языков, в которых бы она была нормально сделана, кроме clojure.

S>Полагаю, что как раз потому, что непонятно, как её синтаксически записать. В кложе-то хорошо — там нет никакого синтаксиса, поэтому испортить его невозможно
S>А как в рамках шарпа написать "хочу хвостовой вызов", чтобы компилятор сразу фейлился если не получилось, и при этом вызов читался бы нормально — вот хз.
например, в F# есть специальные ключевые слова для записи рекурсивных и взаимнорекурсивных функцийй rec/and. Они не гарантируют хвостовую оптимизацию, просто это пример того, как написать "хочу". Возможно, не лучший. Можно описывать хотелку хвостового вызова через атрибуты...
Но тут нам нужен не только лишь способ записать "хочу", а еще и трансформация рекурсивного вызова в явный цикл во время компиляции. Хотя бы на уровне tail recursion trampoline. Сейчас отсутствие хвостовых вызовов с гарантией — это то, что останавливает писать в функциональном стиле на C#.
Ладно факториал... Обидно, если мы делаем обход дерева с continuation passing style и обнаруживаем, что оно все равно переполняет стэк! Иногда весьма внезапно и в продакшне.
Re: Программирование в функциональном стиле на C#
От: vsb Казахстан  
Дата: 24.10.22 11:47
Оценка:
Здравствуйте, Эйнсток Файр, Вы писали:

ЭФ>Возможно ли программировать в функциональном стиле на C# ?


Если там есть оптимизация хвостовой рекурсии, то возможно.
Re[5]: Программирование в функциональном стиле на C#
От: Sinclair Россия https://github.com/evilguest/
Дата: 24.10.22 14:09
Оценка: 4 (1)
Здравствуйте, samius, Вы писали:
S>Так а мы и не про стейт как раз. Если в классическом ФП нет стейта, то лямбда в классическом ФП стейт не заносит. В том же Haskell лямбда — это форма записи фукнции без имени. Анонимная функция. Вполне без них можно обходиться, используя функции с именем. Затруднения будут лишь в случаях, когда нужно записать какой-нибудь высокоуровневый комбинатор. Под стейтом я подразумеваю именно изменяемый стейт, т.е. программирование сайд-эффектов, а не то, что обычно прячется под монадой State, например.

Нет, речь не о сайд-эффектах, а о возможности прикопать "стейт" в функции, которую мы конструируем.
Когда мы делаем
Func<int, int> Increment(int amount) => x => x+amount;

Мы где-то внутри результата вызова Increment(5) эту пятёрку сохраняем. В С это невозможно сделать никаким рассахариванием, т.к. там нет понятия "комбинация из указателя на функцию и значение одного из её аргументов", которая ещё и снаружи выглядит как "функция n-1 аргумента".
То есть нам придётся делать всё вручную.

S>Тут надо договариваться. Когда мы говорим о функциональном стиле, то подразуммеваем сущностное (фактическое отсутствие сайд-эффектов), или красивое (насколько тяжело увидеть фактическое отсутствие сайд-эффектов за килотоннами дешугарнутого кода)??? Я подразумевал первое. Сайд-эффекты не создаются при рассахаривании.

Да, надо договариваться. Вопрос же не в наличии сайд-эффектов как таковых — вон, в С++ возможность объявлять const-мемберы была ещё когда я в школе учился.
Это не делало его ФП-языком. И да, в частности и потому, что "рассахаренный" код там получался весьма себе кошмарно-многословным.

S>>Этот "дешугарнутый" код гораздо ближе к чистому ООП, чем к ФП.

S>>Вот оно, чистое ООП:
S>>
S>>interface IFunction<T, R> 
S>>{
S>>   R Invoke(T arg);
S>>}
S>>interface IPredicate<T> : IFunction<T, bool>{}

S>>class AgeGreaterThanOrEqualFilter: IPredicate<Person>
S>>{
S>>   private int _age;
S>>   public AgeGreaterThanOrEqualFilter(int age) => _age = age;
S>>   public bool Invoke(Person p) => p.Age >= age;
S>>}
S>>...
S>>var adults = people.Filter(new AgeGreaterThanOrEqualFilter(18))
S>>

S>>Классический паттерн "стратегия". Отличия от "дешугарнутой лямбды" минимальны.
S>Я не соглашусь, что это близко к ООП. AgeGreaterThanOrEqualFilter не обладает поведением в смысле ООП. Ведет себя как чистая функция, а не как объект с состоянием.
Ну как же не ведёт — у него есть состояние. То, что оно не меняется в процессе его жизненного цикла — ну и что.
Поведение у него тоже есть — вот он реализует Invoke своим, неведомым коду метода person.Filter образом.

Посмотрите в учебник по паттернам ООП: там в качестве примеров паттерна Стратегия скорее всего будут очень похожие на AgeGreaterThanOrEqualFilter классы.
S>>А вот, чисто для сравниения, функциональный стиль:
S>>
S>>var adults = people.Filter(p=>p.Age>=minAge);
S>>

S>Стиль в смысле "красиво" — да. А в смысле "что там под капотом" — это эквивалентный код.
Ну, так и в любом Хаскеле и flow9 и lisp под капотом будет точно такой же эквивалентный код. Потому, что чудес не бывает, и куда-то мы число 18 должны сохранить. Мы не можем его вхардкодить ни в какую функцию, которая просто примет параметр Person и вернёт bool. Так что там где-то внутри есть указатель на код, и указатель на аргумент. Которые всегда используются совместно.

S>Но при использовании термина "функциональный" в контексте фукнционального программирования как программирования без сайд-эффектов, сишная qsort не может быть фукнциональна даже на уровне "вроде почти всё функионально", т.к. она и есть чистый сайдэффект в отношении того, что она делает над входными даннымми. Да, одним из аргументов стейтлесс-функция, но другим — область памяти для грязи.

Тоже верно. У чистого С ещё и не очень хорошо с клонированием. То есть либо мы будем давать функции qsort ещё и доп. аргумент "склади результат сортировки сюда", что оставит её грязной; либо придётся как-то изобретать способ динамически выделить результат нужного размера.

S>Согласен, что это типичный ООП подход. Но то, что его делает ООП — это сайд эффекты, накопление изменений состояния внутри тел визитеров.

S>Стоит его записать в форме
S>T Visit(Expression)
S>вместо
S>void Visit(Expression)
S>и мы получим что-то близкое к набору стратегий, описанных правилами диспетчеризации, что выглядит вполне функционально в отношении сайд эффетов, но довольно неудобно по сравнению с использованием ПМ.
Ну то есть мы получаем эдакое "секретное ФП", которое отличить от ООП можно только очень тщательным просмотром исходного когда в (бесплодных) поисках изменений состояния; при этом оно ещё и чудовищно многословное.

S>Я согласен, что вывод типов не самый продвинутый, однако, цепочки generic методов с шаблонными параметрами и результатом — вполне удаются без спецификаций в значительном кол-ве случаев.

Это просто вывод типов "для ленивых". Т.е. когда у нас есть взаимосвязи типа T x => x, и все цепочки разматываются в одну сторону, то любой дурак такое сделает.
Интересно, когда у нас есть система уравнений. Ну, вот как в примере выше (шарп не может), или когда мы делаем какую-нибудь штуку вида myobj.Select(x=> ....), при наличии нескольких разных сигнатур у Select.
Вот в таких случаях шарп прекрасно разбирается, что к чему:
class Foo
{
   public T Select<T>(Func<string, T> selector) => selector("Hello");
   public T Select<T>(Func<int, T> selector) => selector(42);
}
...
var t1 = foo.Select(x=> x.Length);
var t2 = foo.Select(x=>x*2);


S>Решение из F# кажется вполне годным.

Ну, интересно. Надо повнимательнее на них посмотреть. Но пока настораживают ограничения — например, не вижу способа построить два DU-типа из одинаковых кейзов.
Типичный случай — редукция. Есть ReducedExpression, с каким-то базовым набором узлов, и полный Expression с расширенным.
Мы пишем функцию Reduce(Expression) -> ReducedExpression.
В F#, как я понимаю, нельзя сделать построить Expression на основе ReducedExpression, и нельзя повторно использовать кейзы Expression в ReducedExpression.

S>анализ полноты свитча, может быть, заработает. Хотелось бы просто короткого способа объявления типа "это или то".

S>Закрытую иерархию я в состоянии описать. Или даже сгенерировать ее описание. Но ведь можно было ранее и Record-ы описывать руками. А теперь для них есть решение, а для дискриминаций — нет.
Да, такое можно прикрутить. Причём выглядит как раз логичным продолжением для рекордов — из них-то строить DU самое то! И синтаксис компактный, и деструктуризация из коробки, и struct/class варианты, и вообще всё хорошо. Короче, успех близок.

S>Наверное, это то, чего я жду. Надо поглядеть, спасибо.


S>например, в F# есть специальные ключевые слова для записи рекурсивных и взаимнорекурсивных функцийй rec/and. Они не гарантируют хвостовую оптимизацию, просто это пример того, как написать "хочу". Возможно, не лучший. Можно описывать хотелку хвостового вызова через атрибуты...

В том-то и дело, что хвостовность является свойством не функции, а конкретного вызова.
Вот именно тут я намереваюсь позвать её tail способом, а в другом месте я могу захотеть её же позвать совершенно обычным образом.

S>Но тут нам нужен не только лишь способ записать "хочу", а еще и трансформация рекурсивного вызова в явный цикл во время компиляции. Хотя бы на уровне tail recursion trampoline.

Это две стороны медальки. "автоматическая" трансформация вызовов в tailcall, теоретически доступная хоть в C#, хоть в JIT, на практике означает лотерею — "думал, что tails, выпало heads".
Поэтому "хочу" должно сразу же ругаться если не получилось — с одной стороны, зато уж потом гарантировать отсутствие стека — с другой стороны.

S>Сейчас отсутствие хвостовых вызовов с гарантией — это то, что останавливает писать в функциональном стиле на C#.

S>Ладно факториал... Обидно, если мы делаем обход дерева с continuation passing style и обнаруживаем, что оно все равно переполняет стэк! Иногда весьма внезапно и в продакшне.

О, было бы прикольно посмотреть пример. А то вроде все слова знакомые, звучит интересно, а в голове картинка не складывается
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[6]: Программирование в функциональном стиле на C#
От: samius Япония http://sams-tricks.blogspot.com
Дата: 24.10.22 19:59
Оценка: 127 (2)
Здравствуйте, Sinclair, Вы писали:

S>Нет, речь не о сайд-эффектах, а о возможности прикопать "стейт" в функции, которую мы конструируем.

S>Когда мы делаем
S>
S>Func<int, int> Increment(int amount) => x => x+amount;
S>

S>Мы где-то внутри результата вызова Increment(5) эту пятёрку сохраняем. В С это невозможно сделать никаким рассахариванием, т.к. там нет понятия "комбинация из указателя на функцию и значение одного из её аргументов", которая ещё и снаружи выглядит как "функция n-1 аргумента".
S>То есть нам придётся делать всё вручную.

Замыкания в C# появились на самой его заре, буквально в версии 2.0 вместе с анонимными методами. Они технически довольно близко к лямбдам в C#, но все-таки ими не являются и под рассахариванием лямбд я подразумевал сведение их к методам с использованием замыканий. Если сравнивать с C# более старых версий (до 2.0), то да, без замыканий не рассахарить в общем случае те лямбды, которые обращаются к контексту.

S>>Тут надо договариваться. Когда мы говорим о функциональном стиле, то подразуммеваем сущностное (фактическое отсутствие сайд-эффектов), или красивое (насколько тяжело увидеть фактическое отсутствие сайд-эффектов за килотоннами дешугарнутого кода)??? Я подразумевал первое. Сайд-эффекты не создаются при рассахаривании.

S>Да, надо договариваться. Вопрос же не в наличии сайд-эффектов как таковых — вон, в С++ возможность объявлять const-мемберы была ещё когда я в школе учился.
И да и нет. Наличие или отсутствие сайд-эффектов — это одно, а возможность объявлять const-мемберы — это что-то совсем другое. Не то, что бы конст-мемберы сильно мешали переписывать данные.
Сайд-эффекты это как раз та линия, которая отделяет создание от изменения, декларации от императива.
S>Это не делало его ФП-языком. И да, в частности и потому, что "рассахаренный" код там получался весьма себе кошмарно-многословным.
Тут надо заметить, что ООП-язык из C++ тоже не далеко сахар.

S>>Я не соглашусь, что это близко к ООП. AgeGreaterThanOrEqualFilter не обладает поведением в смысле ООП. Ведет себя как чистая функция, а не как объект с состоянием.

S>Ну как же не ведёт — у него есть состояние. То, что оно не меняется в процессе его жизненного цикла — ну и что.
S>Поведение у него тоже есть — вот он реализует Invoke своим, неведомым коду метода person.Filter образом.
Ладно, пусть так. Объект с неизменяемым состоянием. Тем более, что есть примеры под носом, например, .NET строки.

S>Посмотрите в учебник по паттернам ООП: там в качестве примеров паттерна Стратегия скорее всего будут очень похожие на AgeGreaterThanOrEqualFilter классы.

Это не факт. AgeGreaterThanOrEqualFilter — это функция в математическом смысле. Проекция множества персон на множество {true, false}. А в учебниках по паттернам там скорее всего будет что-то грязное.
https://springframework.guru/gang-of-four-design-patterns/strategy-pattern/

S>>Стиль в смысле "красиво" — да. А в смысле "что там под капотом" — это эквивалентный код.

S>Ну, так и в любом Хаскеле и flow9 и lisp под капотом будет точно такой же эквивалентный код. Потому, что чудес не бывает, и куда-то мы число 18 должны сохранить. Мы не можем его вхардкодить ни в какую функцию, которая просто примет параметр Person и вернёт bool. Так что там где-то внутри есть указатель на код, и указатель на аргумент. Которые всегда используются совместно.
Вхардкодить не можем, но вполне можем получить указатель на функцию, где оно вхардкожено, а не хранит аргумент. Как, например, скомпилированный Regexp. Просто привел пример, не хочу развивать эту тему.

S>>T Visit(Expression)

S>>вместо
S>>void Visit(Expression)
S>>и мы получим что-то близкое к набору стратегий, описанных правилами диспетчеризации, что выглядит вполне функционально в отношении сайд эффетов, но довольно неудобно по сравнению с использованием ПМ.
S>Ну то есть мы получаем эдакое "секретное ФП", которое отличить от ООП можно только очень тщательным просмотром исходного когда в (бесплодных) поисках изменений состояния; при этом оно ещё и чудовищно многословное.
В каком-то смысле да, мы таким секретным "в некоторой степени ФП" можем программировать на чем угодно практически, просто отказавшись от грязных практик и получив некоторую количественную степень лаконичности при сравнении с достаточно ФП-шными языками.

S>Это просто вывод типов "для ленивых". Т.е. когда у нас есть взаимосвязи типа T x => x, и все цепочки разматываются в одну сторону, то любой дурак такое сделает.

S>Интересно, когда у нас есть система уравнений. Ну, вот как в примере выше (шарп не может), или когда мы делаем какую-нибудь штуку вида myobj.Select(x=> ....), при наличии нескольких разных сигнатур у Select.
S>Вот в таких случаях шарп прекрасно разбирается, что к чему:
S>
S>class Foo
S>{
S>   public T Select<T>(Func<string, T> selector) => selector("Hello");
S>   public T Select<T>(Func<int, T> selector) => selector(42);
S>}
S>...
S>var t1 = foo.Select(x=> x.Length);
S>var t2 = foo.Select(x=>x*2);
S>

Уже забыл, когда мне последний раз не хватало возможностей вывода типов в C# для продакшн кода.

S>>Решение из F# кажется вполне годным.

S>Ну, интересно. Надо повнимательнее на них посмотреть. Но пока настораживают ограничения — например, не вижу способа построить два DU-типа из одинаковых кейзов.
S>Типичный случай — редукция. Есть ReducedExpression, с каким-то базовым набором узлов, и полный Expression с расширенным.
S>Мы пишем функцию Reduce(Expression) -> ReducedExpression.
S>В F#, как я понимаю, нельзя сделать построить Expression на основе ReducedExpression, и нельзя повторно использовать кейзы Expression в ReducedExpression.
Если я правильно все понял, то речь идет о Mutually Recursive Types. Это можно.

S>Да, такое можно прикрутить. Причём выглядит как раз логичным продолжением для рекордов — из них-то строить DU самое то! И синтаксис компактный, и деструктуризация из коробки, и struct/class варианты, и вообще всё хорошо. Короче, успех близок.



S>>например, в F# есть специальные ключевые слова для записи рекурсивных и взаимнорекурсивных функцийй rec/and. Они не гарантируют хвостовую оптимизацию, просто это пример того, как написать "хочу". Возможно, не лучший. Можно описывать хотелку хвостового вызова через атрибуты...

S>В том-то и дело, что хвостовность является свойством не функции, а конкретного вызова.
Это что-то новое для меня.
S>Вот именно тут я намереваюсь позвать её tail способом, а в другом месте я могу захотеть её же позвать совершенно обычным образом.
Если хвостовая рекурсия скомпилирована в цикл, то как ее не вызывай — цикл будет циклом. А если не скомпилирована в цикл и выполняется через рекурсивный вызов себя, то тоже это не зависит от способа вызова.

S>>Но тут нам нужен не только лишь способ записать "хочу", а еще и трансформация рекурсивного вызова в явный цикл во время компиляции. Хотя бы на уровне tail recursion trampoline.

S>Это две стороны медальки. "автоматическая" трансформация вызовов в tailcall, теоретически доступная хоть в C#, хоть в JIT, на практике означает лотерею — "думал, что tails, выпало heads".
В том и дело, что в C# она не доступна даже теоретически. А вот JIT может сделать, а может и отказаться.
S>Поэтому "хочу" должно сразу же ругаться если не получилось — с одной стороны, зато уж потом гарантировать отсутствие стека — с другой стороны.
Да. Именно C# мог бы выполнить трансформацию, так что бы JIT-у ничего не осталось, кроме как вкорячить цикл. C# умеет вещи пострашнее — yield, await...

S>>Сейчас отсутствие хвостовых вызовов с гарантией — это то, что останавливает писать в функциональном стиле на C#.

S>>Ладно факториал... Обидно, если мы делаем обход дерева с continuation passing style и обнаруживаем, что оно все равно переполняет стэк! Иногда весьма внезапно и в продакшне.

S>О, было бы прикольно посмотреть пример. А то вроде все слова знакомые, звучит интересно, а в голове картинка не складывается


class List<T>
{
    public T Head { get; }    
    public List<T> Tail { get; }
    public List(T head, List<T> tail)
    {
        Head = head;
        Tail = tail;
    }
}

List<T> Cons<T>(List<T> list, T value)
    => new List<T>(value, list);

int GetLengthRec<T>(List<T> list)
    => 1 + (list.Tail is null ? 0 : GetLengthRec(list.Tail));
    
int GetLengthTail<T>(List<T> list, int acc)
    => list.Tail is null
        ? acc + 1
        : GetLengthTail(list.Tail, acc+1);
        
int GetLengthCps<T>(List<T> list, Func<int, int> cont)
    =>    list.Tail is null
        ? cont(1)
        : GetLengthCps(list.Tail, x => cont(1 + x));

int GetLengthCps2<T>(List<T> list)
{
    Func<List<T>, Func<int, int>, int> length_cont = null;
    length_cont = (l, cont) =>
          l.Tail is null
            ? cont(1)
            : length_cont(l.Tail, x => cont(1 + x));

    return length_cont(list, x => x);
}

void Main()
{
    var list = Enumerable.Range(0, 10000).Aggregate(default(List<int>), Cons);
    Console.WriteLine(GetLengthRec(list));
    Console.WriteLine(GetLengthTail(list, 0));
    Console.WriteLine(GetLengthCps(list, x => x));
    Console.WriteLine(GetLengthCps2(list));
}
Re[7]: Программирование в функциональном стиле на C#
От: Sinclair Россия https://github.com/evilguest/
Дата: 25.10.22 03:32
Оценка:
Здравствуйте, samius, Вы писали:

S>Замыкания в C# появились на самой его заре, буквально в версии 2.0 вместе с анонимными методами. Они технически довольно близко к лямбдам в C#, но все-таки ими не являются и под рассахариванием лямбд я подразумевал сведение их к методам с использованием замыканий. Если сравнивать с C# более старых версий (до 2.0), то да, без замыканий не рассахарить в общем случае те лямбды, которые обращаются к контексту.

Совершенно верно. Под термином "лямбда" я, конечно же, понимал замыкания. Синтаксис с => конечно же, является более удобным (а синтаксис query comprehension ещё удобнее), но вот как раз он уже менее принципиален.
А вот автоматизация построения замыкания, на мой взгляд, является одним из краеугольных камней функционального стиля.

S>И да и нет. Наличие или отсутствие сайд-эффектов — это одно, а возможность объявлять const-мемберы — это что-то совсем другое. Не то, что бы конст-мемберы сильно мешали переписывать данные.

Ну, да. В каждом языке есть разные уровни контроля. В С вообще ничего нет; в C++ можно выразить намерение не изменять состояние this, в C# у нас есть record struct с его неизменяемостью по умолчанию.
В некоторых ФП языках изменяемости нету совсем, в некоторых её можно "включить" для избранных типов или элементов структуры.

S>Тут надо заметить, что ООП-язык из C++ тоже не далеко сахар.

Ну, в целом ничотак. Для неуправляемого языка.

S>Ладно, пусть так. Объект с неизменяемым состоянием. Тем более, что есть примеры под носом, например, .NET строки.

Да, хотя ООП удивительно слепо к таким особенностям. Считается, что состояние объекта инкапсулировано, и нельзя полагаться на его неизменность ни в каких сценариях. Даже Get-методы мы должны трактовать как чёрные ящики и не полагаться на неизменность объекта после их вызова.
Хотя те же строки представляют тот самый случай, когда неизменяемость крайне полезна.

S>Это не факт. AgeGreaterThanOrEqualFilter — это функция в математическом смысле. Проекция множества персон на множество {true, false}. А в учебниках по паттернам там скорее всего будет что-то грязное.

S>https://springframework.guru/gang-of-four-design-patterns/strategy-pattern/
Да, вы правы — в примере стратегия выводит что-то на консоль, то есть представляет грязный код.
Хотя у меня настолько уже испорчено мышление, что сразу же рука дёргается изменить сигнатуру метода на byte[] encryptData(String plainText), что было бы гораздо более естественно, позволило бы сильно сократить код, убрав дублирование с for (int i = 0; i < cipherText.length; i++) System.out.print(cipherText + " ");, и позволило бы сделать стратегию чистой [i]по крайней мере в этом случае.

S>Вхардкодить не можем, но вполне можем получить указатель на функцию, где оно вхардкожено, а не хранит аргумент. Как, например, скомпилированный Regexp. Просто привел пример, не хочу развивать эту тему.

Ну, ок. Тут, действительно можно утонуть.

S>В каком-то смысле да, мы таким секретным "в некоторой степени ФП" можем программировать на чем угодно практически, просто отказавшись от грязных практик и получив некоторую количественную степень лаконичности при сравнении с достаточно ФП-шными языками.

Тогда возникает вопрос осмысленности. То есть мы получаем многословный код, без достоинств ФП (гарантий-то всё ещё нету) с его недостатками (та же неизменяемость массивов, к примеру, всё ещё требует O(logN) с нехорошим коэффициентом против O(1) с коэффициентом 1 в мутабельном случае).

S>Уже забыл, когда мне последний раз не хватало возможностей вывода типов в C# для продакшн кода.

А, ну так это более-менее всегда так: привыкаешь
Я когда на С++ писал не чувствовал нехватки вывода типов. И когда на Паскале писал тоже не ощущал нехватки перегрузки операторов и обобщённых типов.
Это когда в обратную сторону переключаешься, начинаешь видеть разницу.
Ну, вот в том же flow9 практически никогда не нужно указывать типы-параметры, даже в тех местах, где C# без этого не может.
В С# постоянно приходится писать что-то типа public IReadOnlyDictionary<string, (int min, int max)> VarRanges {get;} = new Dictionary<string, (int min, int max)> VarRanges {get;}. На фоне flow9, где всё это превращается просто в varRanges = makeTree(), выглядит коряво.

S>>>Решение из F# кажется вполне годным.

S>Если я правильно все понял, то речь идет о Mutually Recursive Types. Это можно.
Это о частично пересекающихся списках кейзов. Во flow9 можно так:
BinaryExpression ::= Add, Mul, Div, Sub;
LeftAssociativeExpression ::= Div, Sub;
UnaryExpression ::= Negation, Complement;
Expression ::= BinaryExpression, UnaryExpression;

В таких случаях иногда ломается автовывод типов (если функция возвращает Div или Sub, то непонятно, какой тип результата ей назначать — BinaryExpression или LeftAssociativeExpression), но

S>>В том-то и дело, что хвостовность является свойством не функции, а конкретного вызова.

S>Это что-то новое для меня.
S>>Вот именно тут я намереваюсь позвать её tail способом, а в другом месте я могу захотеть её же позвать совершенно обычным образом.
S>Если хвостовая рекурсия скомпилирована в цикл, то как ее не вызывай — цикл будет циклом. А если не скомпилирована в цикл и выполняется через рекурсивный вызов себя, то тоже это не зависит от способа вызова.
В некотором смысле, хвостовая рекурсия компилируется не в цикл, а в jump. То, что при этом получается цикл — побочный эффект.
А джамп, в свою очередь, возможен тогда, когда у нас нет никакого кода после вызова. Тогда можно не делать последовательность из call xxx; ret, а просто сделать jump xxx, а ret будет вызван из той самой xxx, куда мы прыгнули.
JIT предположительно сам видит такие возможности и использует их; но неуправляемость этого процесса весьма раздражает.
В том же flow9 хвостовая оптимизация есть, и она вроде бы работает, но точно так же неявно. У нас одно из первых лабораторных заданий на нём — написать хвосторекурсивную Фибоначчи.
Ещё пару лет назад мы проверяли, и она работала. А на днях вот дети запускали — и stack overflow на больших значениях . Скорее всего — опять в компиляторе что-то поправили.
Вот прямо бесит это всё. Нет чтобы был способ явно написать "здесь хочу хвостовой вызов". Не получилось — ошибка компиляции, с конкретным местом и пояснением.

S>Да. Именно C# мог бы выполнить трансформацию, так что бы JIT-у ничего не осталось, кроме как вкорячить цикл. C# умеет вещи пострашнее — yield, await...

Ну, давайте придумаем синтаксис и выдвинем proposal

S>
S>class List<T>
S>{
S>    public T Head { get; }    
S>    public List<T> Tail { get; }
S>    public List(T head, List<T> tail)
S>    {
S>        Head = head;
S>        Tail = tail;
S>    }
S>}

S>List<T> Cons<T>(List<T> list, T value)
S>    => new List<T>(value, list);

S>int GetLengthRec<T>(List<T> list)
S>    => 1 + (list.Tail is null ? 0 : GetLengthRec(list.Tail));
    
S>int GetLengthTail<T>(List<T> list, int acc)
S>    => list.Tail is null
S>        ? acc + 1
S>        : GetLengthTail(list.Tail, acc+1);
        
S>int GetLengthCps<T>(List<T> list, Func<int, int> cont)
S>    =>    list.Tail is null
S>        ? cont(1)
S>        : GetLengthCps(list.Tail, x => cont(1 + x));

S>int GetLengthCps2<T>(List<T> list)
S>{
S>    Func<List<T>, Func<int, int>, int> length_cont = null;
S>    length_cont = (l, cont) =>
S>          l.Tail is null
S>            ? cont(1)
S>            : length_cont(l.Tail, x => cont(1 + x));

S>    return length_cont(list, x => x);
S>}

S>void Main()
S>{
S>    var list = Enumerable.Range(0, 10000).Aggregate(default(List<int>), Cons);
S>    Console.WriteLine(GetLengthRec(list));
S>    Console.WriteLine(GetLengthTail(list, 0));
S>    Console.WriteLine(GetLengthCps(list, x => x));
S>    Console.WriteLine(GetLengthCps2(list));
S>}

S>

Прекрасно, помедитирую.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re: Программирование в функциональном стиле на C#
От: dmitry_npi Россия  
Дата: 25.10.22 06:07
Оценка:
Здравствуйте, Эйнсток Файр, Вы писали:

ЭФ>Возможно ли программировать в функциональном стиле на C# ?


А может, не надо? Пожалуйста. Для этого есть F# и другие специальные языки. Извращайтесь там, пожалуйста.

Ладно ещё, когда используются возможности ФП, переехавшие в язык "официально": лямбды, Linq, паттерн матчинг.
Но когда начинают на ровном месте мутить из чего придется монады, иммутабельность, каррирование, мемоизацию и т.п. хрень, то это же ужас.
Читать такой код очень трудно. Может быть, в языках ФП это и выглядит органично и упрощает чтение, но не в C#.
Атмосферная музыка — www.aventuel.net
Re[5]: Программирование в функциональном стиле на C#
От: Pauel Беларусь http://blogs.rsdn.org/ikemefula
Дата: 26.10.22 11:52
Оценка:
Здравствуйте, samius, Вы писали:

S>>Дешугарнутые лямбды — не сахар (pun intended). В классическом ФП без них просто невозможно работать — ведь там нет стейта как такового.

S>Так а мы и не про стейт как раз. Если в классическом ФП нет стейта, то лямбда в классическом ФП стейт не заносит.

Стейт есть/нету в зависимости от конкретного ЯП и реализуется трохи по разному — именованый, неименованый, недетерминированый. В тех, что явной концепции "ячейка" нет, будет вычисляемый стейт. Но вобщем даже в хаскеле, если глянуть сайты про бенчмарки, давно уже понадобавляли все необходимое для стейта, куча интринсиков для оптимизации.
Re[8]: Программирование в функциональном стиле на C#
От: Pauel Беларусь http://blogs.rsdn.org/ikemefula
Дата: 26.10.22 11:57
Оценка:
Здравствуйте, Sinclair, Вы писали:

S>Ну, давайте придумаем синтаксис и выдвинем proposal

...
S>Прекрасно, помедитирую.

Глядя на картинку ниже, куда ты хочешь двигать C# ?


Раз уж в ём есть cell, конкретная реализация time, т.е. он императивной основы, то любые фичи так или иначе будут это эксплуатировать.
Re[9]: Программирование в функциональном стиле на C#
От: Sinclair Россия https://github.com/evilguest/
Дата: 27.10.22 05:39
Оценка: 6 (1)
Здравствуйте, Pauel, Вы писали:

P>Глядя на картинку ниже, куда ты хочешь двигать C# ?

Затрудняюсь ответить.

P>Раз уж в ём есть cell, конкретная реализация time, т.е. он императивной основы, то любые фичи так или иначе будут это эксплуатировать.

Ну вот пока что он едет к некоторой мультипарадигменности. Можно строить часть системы на чистых функциях, другую — на императивном изменяемом состоянии.
Можно их комбинировать. C# выглядит как язык, который преследует прикладные цели — давать решения практических задач.
Идеологические мечты вроде homoiconicity в нём заметной роли не играют.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[10]: Программирование в функциональном стиле на C#
От: Pauel Беларусь http://blogs.rsdn.org/ikemefula
Дата: 27.10.22 08:35
Оценка: :)
Здравствуйте, Sinclair, Вы писали:

P>>Раз уж в ём есть cell, конкретная реализация time, т.е. он императивной основы, то любые фичи так или иначе будут это эксплуатировать.

S>Ну вот пока что он едет к некоторой мультипарадигменности. Можно строить часть системы на чистых функциях, другую — на императивном изменяемом состоянии.
S>Можно их комбинировать. C# выглядит как язык, который преследует прикладные цели — давать решения практических задач.
S>Идеологические мечты вроде homoiconicity в нём заметной роли не играют.

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

И вот здесь C# уже язык предыдущих поколений. Выбросить фичу нельзя, только добавить, а раз так, то это непомерная когнитивная нагрузка на необученых. Начинать новые проекты при этом имеет смысл на чем то, что стоит меньших трудозатрат — golang например.

Может тебе сделать proposal в golang? А то меня пугают тамошние простыни кода
Re: Программирование в функциональном стиле на C#
От: IT Россия linq2db.com
Дата: 30.10.22 20:00
Оценка: +1 :)
Здравствуйте, Эйнсток Файр, Вы писали:

ЭФ>Возможно ли программировать в функциональном стиле на C# ?.


Любая парадигма имеет свою как философию, так и инструментарий. Поэтому отвечая на заданный вопрос хочется уточнить, что именно ТС имеет ввиду.

Программировать в рамках философии ФП можно на любом языке, если в этом языке для этого есть минимально необходимый инструментарий. Инструментарий ФП практически уже весь реализован в C# в том или ином виде. Т.е. на C# в функциональном стиле прогаммировать можно. Но зачем?
Если нам не помогут, то мы тоже никого не пощадим.
Re[7]: Программирование в функциональном стиле на C#
От: Sharov Россия  
Дата: 03.11.22 09:40
Оценка:
Здравствуйте, samius, Вы писали:

S>Замыкания в C# появились на самой его заре, буквально в версии 2.0 вместе с анонимными методами. Они технически довольно близко к лямбдам в C#, но все-таки ими не являются и под рассахариванием лямбд я подразумевал сведение их к методам с использованием замыканий. Если сравнивать с C# более старых версий (до 2.0), то да, без замыканий не рассахарить в общем случае те лямбды, которые обращаются к контексту.


А вот интересно, замыкания -- это когда у нас за кадром создается класс(объект) с функцией и состоянием,
т.е. замыкания проходят по типу класса. А лямбды у нас проходям по типу делегата, т.е. указатель на ф-ию.
Т.е. совершенно другой объект с тз CLR. Верно? (Хотя там все объекты или структуры, но делегаты все-таки
обособленный тип)
Кодом людям нужно помогать!
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.