alexeyfv

Опубликовано

- 4 мин чтения

Превращаем C# в Python, JavaScript и F#

C# Humor
img of Превращаем C# в Python, JavaScript и F#

В С# 14 появился новый синтаксис расширений (extension members), позволяющий добавлять методы, свойства и даже перегружать операторы для существующих типов без создания врапперов и без изменения исходных типов.

Благодаря этому, стал возможен вот такой код:

   var str = "Hello, C# code!"  
        | No  
        | ReplaceToFSharp  
        | ToUpper;

Console.WriteLine(str); // NO! HELLO F#-LIKE CODE!

Выглядит, мягко говоря, необычно, особенно для тех, кто не сталкивался с F# и функциональным программированием в целом. Давайте разберёмся, что тут вообще происходит…

Дисклеймер: Я не призываю никого писать такой код. Статья написана в качестве развлечения, но долей полезной информации.

Блоки расширений

Нововведение, о котором сегодня пойдёт речь, – extension members. Мне больше нравится называть это «блоки расширений», поэтому дальше я буду использовать именно этот термин.

Блоки расширений позволяют определять сразу несколько расширений для одного типа. Например класс c обычными методами расширения:

   public static class Extensions  
{  
    public static bool IsNullOrEmpty(this string source) => string.IsNullOrEmpty(source);  
    public static bool IsNullOrWhiteSpace(this string source) => string.IsNullOrWhiteSpace(source);  
}

теперь записать вот так:

   public static class ExtensionMembers  
{  
    extension(string source)  
    {  
        public bool IsNullOrEmpty() => string.IsNullOrEmpty(source);  
        public bool IsNullOrWhiteSpace() => string.IsNullOrWhiteSpace(source);  
    }  
}

С точки зрения IL это будут всё те же статические методы-расширения.

Но кроме методов, можно определить расширение в виде оператора… И это открывает простор для полёта фантазии. Например, теперь можно написать расширение и умножать строки как в Python:

   Console.WriteLine("C# goes b" + "r" * 10);

public static class ExtensionMembers  
{  
    extension(string source)  
    {  
        public static string operator *(string str, int count)  
        {  
            return string.Concat(Enumerable.Repeat(str, count));  
        }  
    }  
}

Точно так же можно складывать массивы:

   int[] a = [1, 2, 3];  
int[] b = [4, 5, 6];

var concat = a + b;  
Console.WriteLine(string.Join(", ", concat));

public static class ExtensionMembers  
{  
    extension<T>(T[] arr)  
    {  
        public static T[] operator +(T[] a, T[] b)  
        {  
            var result = new T[a.Length + b.Length];  
            Array.Copy(a, result, a.Length);  
            Array.Copy(b, 0, result, a.Length, b.Length);  
            return result;  
        }  
    }  
}

А помните мемы про JavaScript со складыванием и вычитанием строк и чисел? Теперь аналогичное поведение можно реализовать и в C#!

Это C# код, если что

Это C# код, если что

   Console.WriteLine("5" - 3); // 2  
Console.WriteLine("5" + 3); // 53  
Console.WriteLine("10" - "4"); // 6

public static class ExtensionMembers  
{  
    extension(string source)  
    {  
        public static int operator -(string str, int number) => int.Parse(str) - number;  
        public static string operator +(string str, int number) => str + number.ToString();  
        public static int operator -(string a, string b) => int.Parse(a) - int.Parse(b);  
    }  
}

JavaScript разработчикам будет значительно проще вкатиться в С#.

Ну и напоследок – превратим C# в F#. Для этого нужно объявить обобщённое расширение для оператора | (побитовое ИЛИ) и всё – pipe-оператор из говна и палок готов.

   public static class FunctionalExtensions  
{  
    extension<T, TResult>(T)  
    {  
        public static TResult operator |(T source, Func<T, TResult> f) => f(source);  
    }  
}
Двое из ларца - одинаковых с лица

Двое из ларца - одинаковых с лица

Теперь можно объявить несколько статических методов и делать цепочку вызовов функций прямо как в функциональных языках.

   using static StringExtensions;

var str = "Hello, C# code!"  
        | No  
        | ReplaceToFSharp  
        | ToUpper;

Console.WriteLine(str); // NO! HELLO F#-LIKE CODE!

public static class StringExtensions  
{  
    public static string No(string source) => $"No! {source}";  
    public static string ReplaceToFSharp(string source) => source.Replace("C#", "F#-like");  
    public static string ToUpper(string source) => source.ToUpper();  
}

Что же под капотом?

Синтаксический сахар (или синтаксическая соль, тут уж кому как) который мы использовали, компилируется в довольно страшную конструкцию.

Например, код имитирующий F#, упрощённо можно записать вот так.

   var str = FunctionalExtensions.op_BitwiseOr(  
    FunctionalExtensions.op_BitwiseOr(  
        FunctionalExtensions.op_BitwiseOr(  
            "Hello, C# code!",  
            new Func<string, string>(No)  
        ),  
        new Func<string, string>(ReplaceToFSharp)  
    ),  
    new Func<string, string>(ToUpper)  
);

Этот код тоже компилируется и выполняется, потому что компилятор для оператора | генерирует статический дженерик op_BitwiseOr, доступный из пользовательского кода (как обычный метод):

   public static class FunctionalExtensions  
{  
  public static TResult op_BitwiseOr<T, TResult>(T source, Func<T, TResult> f)  
  {  
    return f(source);  
  }  
}

А как было раньше?

Раньше тоже можно было писать похожий код, но если класс недоступен для изменений, как тот же string, то нужно было писать враппер. Пример кода в функциональном стиле с использованием структуры-враппера:

   using static FunctionalString;

var str = new FunctionalString("Hello, C# code!")  
        | No  
        | ReplaceToFSharp  
        | ToUpper;

Console.WriteLine(str); // Output: NO, HELLO F#-LIKE CODE!

public record struct FunctionalString(string Value)  
{  
    public static FunctionalString operator >>(FunctionalString fs, Func<FunctionalString, FunctionalString> f) => f(fs);

    public static implicit operator FunctionalString(string s) => new(s);  
    public static implicit operator string(FunctionalString fs) => fs.Value;

    public static FunctionalString No(FunctionalString fs) => $"No! {fs.Value}";  
    public static FunctionalString ReplaceToFSharp(FunctionalString fs) => fs.Value.Replace("C#", "F#-like");  
    public static FunctionalString ToUpper(FunctionalString fs) => fs.Value.ToUpper();  
}
Осталось только приделать pipe-оператор, каррирование и discriminated unions

Осталось только приделать pipe-оператор, каррирование и discriminated unions

И что будет дальше?

Разработчикам C# осталось лишь добавить pipe-оператор, каррирование, discriminated unions и можно, как минимум, отправлять F# на пенсию.