Pull to refresh

Comments 42

Статья была про "функционал F# перенесенный в С#". Ожидал увидеть как реализуется на F#, а потом аналоги в C#.


Некоторые новые фичи наоборот огорчают, например, описанные выше кортежи/деструктуризация. Если тебе нужно вернуть сложные значения — вводи класс, делай интерфейс, вспоминай Фаулера и рефактори, чтобы код был поддерживаемым, расширяемым и тестируемым… А не вкрячивай Tuple с хитрожопым синтаксисом просто потому что лень. Есть небольшие опасения, что можно C# довести до состояния JS-помойки напихивая в него всё подряд.

вводи класс, делай интерфейс, вспоминай Фаулера и рефактори, чтобы код был поддерживаемым, расширяемым и тестируемым… А не вкрячивай Tuple с хитрожопым синтаксисом просто потому что лень

Поначалу также думал про анонимные типы, потом ничего — втянулся:)

Анонимные типы, которые используются в пределах одного метода — это нормально. Но если возвращать из методов, то имхо та же проблема, что и возврата кортежей — придётся выдавать наружу object/dynamic, абсолютно неподдерживаемо.

А зачем возвращать?
Это же та же ситуация с именованием переменных: если переменная «очень временная», то можно её обозвать коротким именем, не несущим смысла, а если полноценная, будь добр придумать достойное имя.
И анонимные типы, и кортежи расширяют возможности программиста, который обязан пользоваться здравым смыслом.
А зачем возвращать?

Я, наверное, неточно выразился. Про возврат кортежей — это про деструктуризацию. Она, как раз на результаты методов только и работает.

Зависит от метода. Можно попросить компилятор помочь вернуть анонимный тип через дженерики, без всяких путающих кастов. Что-то типа этого:


T SomeMethod(Func<SomeType, T> callback)
{
    ...
    return callback(_someObjOfSomeType);
}

void Main()
{
    var anonObj = SomeMethod(someType => new {X = someType.SomeProp});
    Console.WriteLine(anonObj.X);
}
Для себя взял простое правило и другим советую: использовать кортежи и тому подобные анонимные структуры только в приватных методах.

Кортежи можно рассматривать как альтернативу out-параметрам.
Вот есть же в стандартной библиотеке соглашение об именовании:


bool TryDoSomething(out TResult result)

Но с async/await такой фокус не прокатит: out-параметры запрещены.
А теперь мы сможем написать что-то вроде:


Task<(TResult result, bool ok)> TryDoSomethingAsync()

var (result, ok) = await TryDoSomethingAsync();
if (!ok) { ... }
Здесь лучше использовать паттерн Opition (или Maybe), благо библиотек, его реализующих, тьма — LanguageExt, например

Позвольте с Вами не согласиться. Точечное применение Maybe в императивном коде не даст особых преимуществ по сравнению с кортежами. А если притащить LanguageExt, то весь код придется писать в функциональном стиле. Иначе получится каша.


А писать полностью в функциональной парадигме на C# мало кто может себе позволить:


  • Если у Вас открытый проект – Вы сделаете его API сложным для новичков.
  • Вся Ваша команда должна разбираться в ФП. Потому что без знания какого-то языка заточенного под ФП, и без знания теории, очень трудно понять что происходит в LanguageExt, а главное, зачем.
  • И наконец, если у Вас есть такая команда, почему бы Вам не писать сразу на F# ?

Туплы — это реализация sum type. Используются когда надо запретить каррирование некого набора типов.


То что в C# sum type проще делать через классы — проблема C#

То что в C# sum type проще делать через классы — проблема C#

Это не проблема. При проектировании системы типов C# просто делались другие допущения. И то что логически в F# — sum type, в C# — интерфейс. Про это и речь, не надо тянуть элементы других языков и парадигм туда, где всё спроектировано иначе, в итоге получим мешанину без логики.

Иногда проще в каком-нибудь методе для Dictionary/HashSet заюзать Tuple, чем городить под это дело целый класс с однотипной реализацией override Equals.

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


После разделения на отдельные логические части нам требуется способ обменяться данными. Есть два варианта — предложенные кортежи и куча DTO.


Чем огромная куча DTO лучше? Она волшебным образом делает код "поддерживаемым, расширяемым и тестируемым"? Нет, просто появится кучка новых классов, которые используются в одном месте.

Чем огромная куча DTO лучше? Она волшебным образом делает код "поддерживаемым, расширяемым и тестируемым"? Нет, просто появится кучка новых классов, которые используются в одном месте.

Если у вас классы, которые осуществляют "обмен между отдельными логическими частями" системы используются "в одном месте", то у вас где-то присутствует ошибка проектирования. Интерфейс между логическими частями должен обладать определённой стабильностью и реиспользоваться много-много раз. Иерархия DTO — наследоваться и документироваться. Скорее всего в таком случае у вас "абстракции протекают" и вы выставляете детали реализации наружу посредством динамическим "разовых" данных. В таком случае это надо прятать подальше от глаз в приватные методы/внутренности методов посредством рефакторинга, а не оборачивать синтаксическим сахаром и говорить "вот теперь норм".

У вас ошибка в предположениях, имелись в виду отдельные логические части не системы, но процесса обработки. Одного, но большого. Который вполне себе разделяется на более маленькие части. Хотя они сильно связанные, но их публичными делать не надо. Переиспользоваться 99% из них внешними тоже не будут, ибо они имеют смысл только в контексте данного процесса. И из-за переменчивой структуры реальности смысла вводить жесткий интерфейс между ними нет.


Но даже если рассматривать более большие части, в чем принципиальное преимущество DTO перед именованными кортежами из двух-трех значений?


Их можно наследовать? На мой взгляд, если вам нужно наследовать DTO, то всё свернуло куда-то не туда. Наследование вообще нужно применять минимально и стараться избегать наследования данных. А так как DTO из себя представляют чистые данные, которые кто-то запихнул в оболочку объекта (из-за отсутсвия более подходящих средств для их удержания), то наследовать их не надо.


Их можно документировать? Да, возможно, но какой в этом смысл? Если у вас DTO из двух-трех членов требует документации, то возникают вопросы "а что эти данные вообще делают вместе" и "а не пора ли это отрефакторить"

«вводи класс, делай интерфейс»… интерфейс не нужен когда речь идет о структурных immutable типах, что касется классов в них тоже может не быть необходимости. Например на уровне абстрактных библиотек ввода/вывода: вы можете предоставлять пользователю возможность считать из потока три записи byte[] Read(), затем предлогая пользователю самому привести их к ожидаемым типам, а можете предоставлять возможность считать кортеж (задавая спецификацию приведения типов как generic параметры: Tuple<int, string, double> Read<int, string, double>() ). Тоже самое можно сделать и передавая через generic параметер любой class и используя reflection обойти его свойства, но на мой взгляд это будет гораздо более медленно, многословно и ненадежно. Неужели вы так и поступите?

Причем тут рефлексия? Что вам мешает вместо Tuple<int, string, double> использовать


IReadResult
{ 
int Length {get;set;}
string LogicName {get;set;}
double Length {get;set;}
 }

? Например IReadResult Read()?
Во-первых, логичные имена переменных.
Во-вторых, операцию ввода-вывода вы сможете нормально замокать и протестить родительский метод.
В-третьих, можно будет отнаследоваться от интерфейса и расширить эту операцию в дочерних классах.
В-четвертых, возможна инкапсуляция некоторой логики в свойствах.

Я имел ввиду абстрактную функциональность ввода вывода, которая может возвратить разные кортежи разной длины. Семью функций T1 Read>T1>(), Tuple<T1,T2> Read<T1, T2>() ,Tuple<T1,T2,T3> Read<T1, T2,T3>() и т.д.
А так вы правы, что Reflection не обязателен, возможно чтобы вернуть IReadResult передавать еще и «DataAdapter», где руками прописать как порождать IReadResult из трех byte[]. Все же это будет сложнее, чем задать спецификацию через generic параметры.

Пришел в голову еще один арумент, если первая функция T1 Read >T1>() не вызывает паники то и остальные (с кортежами, типа Tuple<T1,T2,T3,T4> Read<T1,T2,T3,T4>() ) не должны, они просто логическое ее продолжение.

Логичные имена дать можно var (id, type, weight) = Read<int, string, double>(); а про замокать, отнаследовать и инкапсулировать — повторю, далеко не всем необходимо иметь возможность мокать, наследовать и инкапсулировать иммутабельные структуры данных.
виду абстрактную функциональность ввода вывода, которая может возвратить разные кортежи разной длины
если первая функция T1 Read >T1>() не вызывает паники то и остальные (с кортежами, типа Tuple<T1,T2,T3,T4> Read<T1,T2,T3,T4>() ) не должны, они просто логическое ее продолжение.

Вот как раз немного вызывает панику. :) Почему? Потому что в методе T1 Read<T1>() where T1: class как ты не меняй внутренности класса T1 сигнатура и контракт метода остается одинаковым. Что позволяет не переписывать тонну кода, которые с этим связано и проектировать систему внешних классов на этом интерфейсе. А вот при использовании примитивов и переменного количества аргументов как минимум это будет плыть.

T1,T2,T3 тут примитивы, поэтому за фразой «как ты не меняй внутренности класса T1» я опять не вижу никакой реальной проблемы. если поменялся протокол потока (добавилось поле в считываемый рекордсет) то переписал и спецификацию считывания: было Tuple<T1,T2,T3> Read<T1,T2,T3>() стало Tuple<T1,T2,T3,T4> Read<T1,T2,T3,T4>() У интерфейса построенного на кортежах не может поплыть более чем у пользователе интерфейса построенного на class и dataadapter.

Я понимаю так, что ваша озадаченность, это вопрос: «а что вы потом с кортежем делать будете», и далее сами отвечате: «вот были бы классы могли бы вовне скинуть»! Ответ: во-первых если не уходит IReadResult во вне то зачем его объявлять (например потому что то к тому что мы считали, должны добавить четвертое поле dateTime — и это другой class), и во-вторых не всегда внешней системе нужны C# classы, например при сериализации в json во внешнюю систему уйдет {5,«typeA»,0.1123} в любом случае, сериализуйте вы instance class или tuple.

Пример кода, для конкретики.

var source = transaction.ReadList<int, string, double>("SELECT id, type, weight FROM dbo.ITEM_VIEW");
foreach ( var (id, type, weight) in source)
{
    //...
}


Я понимаю какие вопросы могут быть к такому Data Access Layer, и наверно ими можно оправдать замечания к языку «новые фичи которые огорчают», тем не мнее, программисты с удовольствием будут использовать кортежи где архитектура им это позволяет.
Как мне кажется, введение методов по умолчанию в интерфейсах — это бред. Есть же абстрактные классы?
Можно было бы ввести концепты, трейты, методы расширения для всего, как предлагали на github, но вводить реализацию по умолчанию в интерфейсы(которые как бы контракт) — какой-то бред.
введение методов по умолчанию в интерфейсах — это бред. Есть же абстрактные классы?

Если что, напоминаю, что в c# множественного наследования НЕТ.

Хотелось бы вместо Pattern Matching получить Smart Cast:
Pattern Matching:


object some = other();
if (some is string s)
    return s.Length;

Smart Cast:


object some = other();
if (some is string)
    return some.Length;
Да пользуюсь этим в type script, очень удобно.

Эрик Липперт где-то писал почему этого не сделали, а вообще я тоже хотел эту фичу (понравилась в котлине) но вариант с ПМ не сильно многословнее

Возникает неопределённость в случае явно (explicit) реализованных методов интерфейса. Конечно, писать разный код для методов с одинаковой сигнатурой — это те ещё грабли, но такая фича есть и теперь из песни, как говорится, слов не выкинешь.
Иммутабельность
Это ни что иное, как неизменяемость объектов.

А чем это отличается от старых добрых констант const?
(реально любопытно)

const в C# используется для констант времени компиляции, здесь же после установки свойства при создании объекта оно больше не будет меняться

То что объявлено как const, разве можно потом изменять? (O_O)

То что объявлено как const нужно инициализировать сразу.

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


const int c = 3;
...
var x = c; // компилятор подставит 3 на место c
...
object p { get; } = new object();
...
var y = p; // произойдет обращение к свойству
Это просто синтаксический сахар, упрощающий такую конструкцию:
readonly string _s;
public string S => _s;
До такой:
public string S { get; }
А еще можно написать extension для KeyValuePair который будет декомпозировать его:
public static class KeyValueExt 
{
    public static void Deconstruct<K,V>(this KeyValuePair<K,V> pair, out K key, out V val) 
    {
        key = pair.Key;
        val = pair.Value;
    }
    
    public static void Test()
    {
        var dict = new Dictionary<string, int> { {"123", 213} };
        
        foreach(var (k,v) in dict)
        {
            var s = $"key {k}, value {v}";
        }
    } 
}
Если уж заговорили про Java, то я из «вражеского лагеря» забрал бы их функционал Enum.

Надо забирать discriminated union из F#. Очень.

Стоит ещё упомянуть различные implicit в scala…
Очень мечтаю о Case-классах и автогенерируемом copy() в частности.
Sign up to leave a comment.

Articles