Здравствуйте, sergii.p, Вы писали:
SP>А если сравнивать ООП с ФП, то иммутабельность видимо не является принципиальным отличием.
Является.
SP>Ведь без изменения состояния можно работать и в ООП парадигме.
Можно, но тогда от ООП почти ничего не остаётся.
SP>Принципиальным отличием, как мне кажется, является то как мы вносим зависимости.
SP>В ООП через интерфейсы:
SP>SP>struct Out { virtual void write(const Data&) const = 0; };
SP>struct Console { virtual void write(const Data&) const { ... }; };
SP>struct File { virtual void write(const Data&) const { ... }; };
SP>void foo(const Data& d, Out& out) {
SP> out.write(d);
SP>}
SP>void main(){
SP> const auto console = Console{};
SP> const auto file = File{};
SP> const Data d{42};
SP> foo(d, console);
SP> foo(d, file);
SP>}
SP>
Здесь нет никаких интерфейсов; языки с номинативной типизацией (например, C или C++) просто не скомпилируют этот код. Для того, чтобы в foo можно было передавать Console и File, нам нужно унаследовать их от Out.
SP>В ФП — через функции:
Не обязательно. Вот вы в том, что вы назвали "ООП", просто завернули функции write внутрь структур.
В ФП вы можете сделать примерно то же самое — заверните функцию внутрь структуры:
struct Out { write: (Data) -> void}
То, что в ФП это кажется оверкиллом — ну, так это оттого, что вы выбрали вырожденный интерфейс.
Возьмите в качестве интерфейса что-нибудь поинтереснее — например, пару из NeutralElement и Combine:
public interface IGroup<T>
{
public T NeutralElement {get;}
public T Combine(T a, T b)
}
public class IntAddition: IGroup<int>
{
public int NeutralElement { get => 0; }
public int Combine(int a, int b) => a + b;
}
public static class H
{
public static T Reduce<T>(this IEnumerable<T> input, IGroup<T> group)
{
var r = group.NeutralElement;
foreach(var i in input)
r = group.Combine(r, i);
return r;
}
}
public static class Program
{
public static void Main()
{
var t = new[] {4, 8, 15, 16, 23, 42};
var sum = t.Reduce(new IntAddition());
}
}
Вот вам ООП подход.
В ФП вместо ООП тут будет не функция, а структура, с точно такой же топологией.
public static T Reduce(this IEnumerable<T> input, (Func<T> neutralElement, Func<T, T, T> combine))
{
var r = group.neutralElement();
foreach(var i in input)
r = group.combine(r, i);
return r;
}
public static class Program
{
public static void Main()
{
var t = new[] {4, 8, 15, 16, 23, 42};
var intAddition = (()=>0, (int x, int y)=>x+y);
var sum = t.Reduce(intAddition);
}
}
Видим, что всё работает точно так же, как и следовало ожидать. То есть мы по-прежнему инжектим зависимость через "интерфейс", просто теперь это не какая-то специальная конструкция в языке, а просто обычная неизменяемая структура с полями-функциями.
Отличия начинаются ровно в том месте, где у нас члены этого интерфейса начинают
что-то менять.
например, так:
public interface IAccumulator<T>
{
public T Current{get;}
public void Accumulate(T value);
}
public class AddAccumulator: IAccumulator<int>
{
private _value = 0;
public int Current{ get => _value; }
public void Accumulate(int value) => _value += value;
}
public static class H
{
public static T Reduce<T>(this IEnumerable<T> input, IAccumulator<T> accumulator)
{
foreach(var i in input)
accumulator.Accumulate(i);
return accumulator.Current;
}
}
public static class Program
{
public static void Main()
{
var t = new[] {4, 8, 15, 16, 23, 42};
var sum = t.Reduce(new AddAccumulator());
}
}
Вот для такого кода написать прямой аналог на ФП уже не получится, потому что в каноническом ФП мы не можем менять состояние существующих объектов.
Строгое ФП потребует от нас поменять сигнатуры методов и слегка переколбасить код.
Что-то вроде
public struct record Accumulator<T>(T value, Func<T, Accumulator<T>> accumulate);
public static class H
{
public static T Reduce<T>(this IEnumerable<T> input, Accumulator accumulator)
{
foreach(var i in input)
accumulator = accumulator.Accumulate(i);
return accumulator.Current;
}
public static Accumulator<T> CreateAccumulator<T>(T value, Func<T, T, T> combine)
=> new Accumulator(value, (T x) => CreateAccumulator(combine(value, x), combine);
}
public static class Program
{
public static void Main()
{
var t = new[] {4, 8, 15, 16, 23, 42};
var addAccumulator = H.CreateAccumulator(0, (x, y)=> x+y);
var sum = t.Reduce(addAccumulator);
}
}
Вот как раз тут видно, почему ООП без изменяемого состояния — "не ООП".