F # — это функциональный язык программирования, который компилируется в .NET Intermediate Language (IL). C# становится более функциональным языком программирования. В последней версии C # (9) есть новые функции, которые делают функциональное программирование более доступным. Хорошая новость в том, что поскольку оба языка компилируются в IL, мы можем использовать их как взаимозаменяемые. Мы можем ссылаться на проекты F # в проектах C # и наоборот. Кроме того, с помощью dnSpy мы можем преобразовать IL в C #. В этой статье объясняется, как скомпилировать сборку F # (IL), а затем ссылаться на нее из C # или преобразовать ее в C #. Это дает программистам на C # богатый набор функций F # без необходимости переносить весь код.

Что такое функциональное программирование?

Текст Википедии это хорошо объясняет.

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

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

Обычно мы делим языки на две категории: функциональные и императивные. Императивный подход фокусируется на предоставлении пошаговых инструкций в циклах, в то время как функциональные языки полагаются на выражения и концепцию функций более высокого порядка для более декларативного стиля. Обычно мы называем C # императивным языком, а F # функциональным языком, но вы должны понимать, что ни один язык не является чисто функциональным или императивным, и оба имеют императивные и функциональные конструкции. Таким образом, категоризация любого из них может ввести в заблуждение. В C # вы можете смешивать и сочетать подходы. Пуристы возразят, что смешивание подходов — плохая идея, но, как вы увидите, вы, вероятно, уже используете некоторые функциональные конструкции C #. Вам и вашей команде решать, какой функциональный подход вы используете с кодом C #.

Функции высшего порядка

Как упоминалось выше, функция высшего порядка — это функция, которая принимает одну или несколько функций в качестве параметра или возвращает функцию. Если вы использовали лямбда выражения LINQ в C #, вы, вероятно, использовали функции высшего порядка. Например, Enumerable.Where — это функция высшего порядка. Он принимает функцию в качестве параметра. Вы можете сохранить функцию как переменную и передать ее методу Where. Например, этот метод возвращает только строки длиной в один символ.

public IList<string> GetFilteredStrings()
{
    Func<string, bool> predicate = a => a.Length == 1;
    return new List<string> { "321", "21", "1" }.Where(predicate).ToList();
}

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

Это только один аспект функционального программирования. Функциональное программирование — это целая парадигма, которая побуждает программистов подходить к программированию совершенно по-другому. F # настоятельно рекомендует функциональное программирование с нуля. С другой стороны, C # — гибкий язык, допускающий функциональные, императивные и объектно-ориентированные конструкции.

Expressions против. Statments

Короче говоря, в функциональном программировании используются выражения, а в императивном программировании — операторы. Операторы сообщают компилятору, что делать, шаг за шагом, в то время как выражения возвращают значения, не обязательно требуя последовательного определения. Рассмотрим эти два метода C #. Оба достигают одного и того же результата, но метод, использующий операторы, гораздо более ясен и подробен о том, что он делает.

public static string ToUppercaseExpression(this string currentString) =>
    new string(currentString.Select(c => (char)(c < 97 || c > 122 ? c : c - 32)).ToArray());

public static string ToUppercaseStatements(this string currentString)
{
    var returnValue = new StringBuilder();
    foreach(var c in currentString)
    {
        if(c < 97 || c > 122)
        {
            returnValue.Append(c);
        }
        else
        {
            returnValue.Append((char)(c - 32));
        }
    }
    return returnValue.ToString();
}

Выражения в F # изящны, и C # заимствует из него конструкции. Элегантность выражений F # может быть хорошей причиной для создания части вашего кода на F #. SQL очень быстро сопоставляется с выражениями запросов F #.

let query1 =
    query {
        for customer in db.Customers do
            select customer
    }

Неизменность

Еще одно ключевое понятие в мире функционального программирования — неизменяемость. По сути, это означает, что после создания переменной ее данные не должны изменяться. Что еще более важно, язык или среда выполнения должны предоставлять инструменты, гарантирующие, что данные не изменятся. Мы используем типы записей в C # и F # для этой цели и неизменяемые списки для коллекций данных. Записи принимают свои значения во время построения и не предоставляют механизма для изменения данных после построения.

Вот пример записи на F #. Очень просто, этот тип принимает три целых числа в качестве параметров конструктора, и вы не можете изменить эти значения после построения.

type Point = { X: int; Y: int; Z: int; }

Это использование в F#.

let point = { X = 1; Y = 2; Z = 3; }

Это использование в C #. Да, вы можете напрямую использовать эти типы из C #.

var point = new FSharpLibrary.Point(1, 2, 3);

C # 9 добавляет типы записей. Это пример записи на C #:

public record CsharpPoint(int X, int Y, int Z);

Использование:

var csharpPoint = new CsharpPoint(1, 2, 3);

Хотя в C # с самого начала были функции высшего порядка, добавление неизменяемости с помощью типов записей является явным признаком того, что разработчики языка намеренно добавляют в язык больше функциональных конструкций. C #, вероятно, будет развиваться в этом направлении, и программистам на C #, вероятно, потребуется больше узнать о функциональном программировании.

F # Списки

Списки F # по умолчанию неизменяемы. Вы не можете добавлять или удалять элементы из списка без создания нового списка. Вы можете напрямую использовать неизменяемые списки F # в C #. Вам даже не нужно добавлять проект F #. Чтобы использовать списки F # в C #, добавьте пакет Nuget FSharp.Core в свой проект C #. Вот пример консольного приложения C #.

using Microsoft.FSharp.Collections;
using System;

namespace ConsoleApp2
{
    class Program
    {
        static void Main(string[] args)
        {
            var numbers = FSharpList<int>.Cons(
            1,
            FSharpList<int>.Cons(2,
                FSharpList<int>.Cons(
                    3,
                    FSharpList<int>.Empty)));

            Console.WriteLine(string.Join(", ", numbers));
        }
    }
}

Хотя в C # с самого начала были функции высшего порядка, добавление неизменяемости с помощью типов записей является явным признаком того, что разработчики языка намеренно добавляют в язык больше функциональных конструкций. C #, вероятно, будет развиваться в этом направлении, и программистам на C #, вероятно, потребуется больше узнать о функциональном программировании.

F # Списки

Списки F # по умолчанию неизменяемы. Вы не можете добавлять или удалять элементы из списка без создания нового списка. Вы можете напрямую использовать неизменяемые списки F # в C #. Вам даже не нужно добавлять проект F #. Чтобы использовать списки F # в C #, добавьте пакет Nuget FSharp.Core в свой проект C #. Вот пример консольного приложения C #.

using Microsoft.FSharp.Collections;
using System;

namespace ConsoleApp2
{
    class Program
    {
        static void Main(string[] args)
        {
            var numbers = FSharpList<int>.Cons(
            1,
            FSharpList<int>.Cons(2,
                FSharpList<int>.Cons(
                    3,
                    FSharpList<int>.Empty)));

            Console.WriteLine(string.Join(", ", numbers));
        }
    }
}

Выход

1, 2, 3

Примечание. Стандарт C # для неизменяемых списков — ImmutableList. Вы можете прочитать о неизменяемых коллекциях в C # здесь. Он является частью пакета NuGet System.Collections.Immutable.

Дискриминационные союзы

Дискриминационные союзы похожи на наследование в объектно-ориентированном программировании. Вот пример на F #.

type Shape =
    | Rectangle of width : float * length : float
    | Circle of radius : float
    | Prism of width : float * float * height : float

Вы можете напрямую использовать это в C #

static void Main(string[] args)
{
    FSharpLibrary.Shape shape = FSharpLibrary.Shape.NewCircle(1);
    Console.WriteLine($"Is this a rectangle: {shape.IsRectangle}");
}

Как ссылаться на код F # в C #

Скомпилированные сборки F # в основном такие же, как сборки C #. Единственное отличие состоит в том, что сборки F # по умолчанию зависят от библиотеки FSharp.Core. Это просто пакет NuGet, на который можно ссылаться с C #. Вы можете взять этот образец здесь.

Во-первых, убедитесь, что у вас установлены инструменты F #. Создание библиотеки классов F # .NET

Как ссылаться на код F # в C #

Скомпилированные сборки F # в основном такие же, как сборки C #. Единственное отличие состоит в том, что сборки F # по умолчанию зависят от библиотеки FSharp.Core. Это просто пакет NuGet, на который можно ссылаться с C #. Вы можете взять этот образец здесь.

Во-первых, убедитесь, что у вас установлены инструменты F #. Создание библиотеки классов F # .NET

Переход на целевой .NET Standard 2.1

Создайте консольное приложение C # .NET Core (3.1), если у вас еще нет проекта, из которого вы хотите использовать F #.

Ссылка на проект F # из проекта C #.

Теперь вы можете напрямую использовать код из библиотеки F # в своем консольном приложении C #. Это так просто.

Как преобразовать F # в C #

F # компилируется в .NET IL, и вы можете использовать dnSpy для преобразования IL в C #. Это замечательно по двум причинам. Во-первых, F # полностью прозрачен. Вам не нужно гадать, что он делает. Вы можете посмотреть версию C # и увидеть, как она работает. Во-вторых, вы можете использовать скомпилированный код F # в качестве резака для кода C #. Вы можете оказаться в ситуации, когда хотите создать функциональный код с F #, но не можете использовать F # в своей команде. В этом случае вы можете написать F # и перетащить полученный C # в свой проект.

  • Скачайте и установите dnSpy. Это отличный инструмент, который вы должны использовать для проверки кода внутри .NET DLL.
  • Скомпилируйте свой проект на F #
  • Откройте скомпилированную сборку в папке bin в dnSpy
  • Вы увидите структуру IL, преобразованную в C #
  • Вы можете напрямую скопировать и вставить этот код в проект C # или экспортировать весь код как C #.

Вот как выглядит запись типа F # Point при открытии в dnSpy и преобразовании в C #

То же самое с записью C # и просмотренной в dnSpy.

Нажмите на эти изображения, чтобы увидеть весь код. Найдите время, чтобы сравнить записи C # с записями F #. Важно понимать, что оба языка скрывают некоторую сложность, и вы не обязательно об этом узнаете.

Означает ли это, что F # не нужен?

Нет. F # — отличный язык. Как видите, писать код в функциональном стиле на F # гораздо проще, чем на C #. Он настолько прост, что любой, кто понимает C #, может начать использовать его, ничего не зная о языке. Если вы уже пишете код C # в функциональном стиле, вы, вероятно, напишете намного меньше кода, переместив фрагменты на F #.

Смешивание и подбор

F # более самоуверенный язык, чем C #. Как видите, он скрывает большую часть сложности типов, которые мы видим в C #. Между C # и F # также много дублирования. Списки F # не то же самое, что неизменяемые списки в System.Collections.Immutable, и записи внутри тоже не совпадают. Итак, безопасно ли использовать типы F # в C #?

Ответ зависит от вашего проекта и вашей команды. Прозрачность важна, потому что типы F # могут вести себя не так, как ожидалось. Разработчики C # могут запутаться в своих тонких различиях. Если вы используете типы F #, убедитесь, что ваша команда согласна с этим, и это хорошо понимают. Не используйте типы F # без надобности. Например, у вас, вероятно, будет меньше проблем с записями C #, чем с записями F # в C #. Но если вы создаете всю библиотеку с помощью F #, нет причин, по которым вы не можете использовать это с помощью C #. Кроме того, нет причин, по которым вы не можете использовать библиотеки, такие как System.Collections.Immutable, в F #, чтобы неизменяемые списки были совместимы с C #.

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

Остановить трайбализм

Ни одна группа не обладает монополией на функциональное программирование. F # — отличный инструмент для написания функционального кода, когда вы можете его использовать, но это не значит, что вам не следует использовать программирование в функциональном стиле на C #. Если вы программист на F # и работаете над проектом только с C #, объясните, как можно использовать F # в существующей инфраструктуре C #. Если вы программист на C #, остановитесь и подумайте обо всем программировании функционального стиля, которое вы уже сделали. Лямбда-выражения не существовали бы, если бы функциональные сторонники не настаивали на функциональном программировании в пространстве C #. C # просто не является чисто императивным или объектно-ориентированным языком. Он явно имеет аспекты нескольких парадигм, и вы не получите преимуществ языка и платформы, если откажетесь от программирования в функциональном стиле.

В экосистеме .NET нет места антагонизму между группами программистов. Это оба инструмента, которые мы можем использовать для решения проблем. Перестаньте думать о себе как о разработчике C # или F # и думайте о себе как о .NET-разработчике. Оба языка дополняют .NET.

Резюме

И F #, и C # компилируются в библиотеки .NET. Вы можете создать код для F # на C #, и вы можете создать код для C # на F #. Вы можете преобразовать код F # в C # с помощью dnSpy. Будущее C # — за функциональностью. F # — еще один инструмент в вашем арсенале. Смотрите на языки как на взаимодополняющие, а не исключающие друг друга. Используйте F #. Не бойся этого. Будет полезно немного узнать об этом, даже если вы не программируете на чистом F #. Если можете, попробуйте разбить некоторые из ваших библиотек на F #. Вы можете обнаружить, что требуется меньше кода, и вы можете найти неожиданные преимущества от более функционального подхода. Обсуждая функциональное программирование в вашей команде, начните с C #, и некоторые люди могут с энтузиазмом относиться к переносу частей кодовой базы на F #.