Какую задачу лучше всего выполнить в стиле функционального программирования? - PullRequest
28 голосов
/ 29 марта 2009

Я только недавно открыл стиль функционального программирования, и я убежден, что он сократит усилия по разработке, облегчит чтение кода, сделает программное обеспечение более легким в обслуживании. Однако проблема в том, что я никого не убедил.

Что ж, недавно мне дали возможность выступить с докладом о том, как сократить усилия по разработке и сопровождению программного обеспечения, и я хотел представить им концепцию функционального программирования и ее пользу для команды. У меня была идея показать людям 2 набора кода, которые делают одно и то же, один код был написан очень настоятельно, а другой - очень функционально, чтобы показать, что функциональное программирование может сделать код намного короче, легче понять таким образом, ремонтопригодны. Есть ли такой пример, кроме знаменитого примера суммы квадратов Луки Болоньезе?

Ответы [ 16 ]

64 голосов
/ 29 марта 2009

Я только недавно открыл стиль функционального программирования [...] Ну, недавно мне дали возможность рассказать о том, как уменьшить усилия по разработке программного обеспечения, и я хотел представить концепцию функциональное программирование.

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

Я рекомендую вам сделать то же самое: подождите 6 месяцев или около того, пока стиль функционального программирования не станет более естественным, а затем представьте свою презентацию.

Я пытаюсь проиллюстрировать преимущества функциональных программирование, и у меня была идея показывая людям 2 набора кода, который делает то же самое, один закодирован в очень императивным способом, а другой в очень функциональный способ, чтобы показать, что функциональное программирование может сделать код короче, легче понять и таким образом поддерживать. Есть ли такой пример, рядом со знаменитой суммой квадратов пример Луки Болоньезе?

Я провел презентацию F # для группы пользователей .NET в моем регионе, и многие люди в моей группе были впечатлены сопоставлением с образцом F #. В частности, я показал, как обходить абстрактное синтаксическое дерево в C # и F #:

using System;

namespace ConsoleApplication1
{
    public interface IExprVisitor<t>
    {
        t Visit(TrueExpr expr);
        t Visit(And expr);
        t Visit(Nand expr);
        t Visit(Or expr);
        t Visit(Xor expr);
        t Visit(Not expr);

    }

    public abstract class Expr
    {
        public abstract t Accept<t>(IExprVisitor<t> visitor);
    }

    public abstract class UnaryOp : Expr
    {
        public Expr First { get; private set; }
        public UnaryOp(Expr first)
        {
            this.First = first;
        }
    }

    public abstract class BinExpr : Expr
    {
        public Expr First { get; private set; }
        public Expr Second { get; private set; }

        public BinExpr(Expr first, Expr second)
        {
            this.First = first;
            this.Second = second;
        }
    }

    public class TrueExpr : Expr
    {
        public override t Accept<t>(IExprVisitor<t> visitor)
        {
            return visitor.Visit(this);
        }
    }

    public class And : BinExpr
    {
        public And(Expr first, Expr second) : base(first, second) { }
        public override t Accept<t>(IExprVisitor<t> visitor)
        {
            return visitor.Visit(this);
        }
    }

    public class Nand : BinExpr
    {
        public Nand(Expr first, Expr second) : base(first, second) { }
        public override t Accept<t>(IExprVisitor<t> visitor)
        {
            return visitor.Visit(this);
        }
    }

    public class Or : BinExpr
    {
        public Or(Expr first, Expr second) : base(first, second) { }
        public override t Accept<t>(IExprVisitor<t> visitor)
        {
            return visitor.Visit(this);
        }
    }

    public class Xor : BinExpr
    {
        public Xor(Expr first, Expr second) : base(first, second) { }
        public override t Accept<t>(IExprVisitor<t> visitor)
        {
            return visitor.Visit(this);
        }
    }

    public class Not : UnaryOp
    {
        public Not(Expr first) : base(first) { }
        public override t Accept<t>(IExprVisitor<t> visitor)
        {
            return visitor.Visit(this);
        }
    }

    public class EvalVisitor : IExprVisitor<bool>
    {
        public bool Visit(TrueExpr expr)
        {
            return true;
        }

        public bool Visit(And expr)
        {
            return Eval(expr.First) && Eval(expr.Second);
        }

        public bool Visit(Nand expr)
        {
            return !(Eval(expr.First) && Eval(expr.Second));
        }

        public bool Visit(Or expr)
        {
            return Eval(expr.First) || Eval(expr.Second);
        }

        public bool Visit(Xor expr)
        {
            return Eval(expr.First) ^ Eval(expr.Second);
        }

        public bool Visit(Not expr)
        {
            return !Eval(expr.First);
        }

        public bool Eval(Expr expr)
        {
            return expr.Accept(this);
        }
    }

    public class PrettyPrintVisitor : IExprVisitor<string>
    {
        public string Visit(TrueExpr expr)
        {
            return "True";
        }

        public string Visit(And expr)
        {
            return string.Format("({0}) AND ({1})", expr.First.Accept(this), expr.Second.Accept(this));
        }

        public string Visit(Nand expr)
        {
            return string.Format("({0}) NAND ({1})", expr.First.Accept(this), expr.Second.Accept(this));
        }

        public string Visit(Or expr)
        {
            return string.Format("({0}) OR ({1})", expr.First.Accept(this), expr.Second.Accept(this));
        }

        public string Visit(Xor expr)
        {
            return string.Format("({0}) XOR ({1})", expr.First.Accept(this), expr.Second.Accept(this));
        }

        public string Visit(Not expr)
        {
            return string.Format("Not ({0})", expr.First.Accept(this));
        }

        public string Pretty(Expr expr)
        {
            return expr.Accept(this).Replace("(True)", "True");
        }
    }

    class Program
    {
        static void TestLogicalEquivalence(Expr first, Expr second)
        {
            var prettyPrinter = new PrettyPrintVisitor();
            var eval = new EvalVisitor();
            var evalFirst = eval.Eval(first);
            var evalSecond = eval.Eval(second);

            Console.WriteLine("Testing expressions:");
            Console.WriteLine("    First  = {0}", prettyPrinter.Pretty(first));
            Console.WriteLine("        Eval(First):  {0}", evalFirst);
            Console.WriteLine("    Second = {0}", prettyPrinter.Pretty(second));
            Console.WriteLine("        Eval(Second): {0}", evalSecond);;
            Console.WriteLine("    Equivalent? {0}", evalFirst == evalSecond);
            Console.WriteLine();
        }

        static void Main(string[] args)
        {
            var P = new TrueExpr();
            var Q = new Not(new TrueExpr());

            TestLogicalEquivalence(P, Q);

            TestLogicalEquivalence(
                new Not(P),
                new Nand(P, P));

            TestLogicalEquivalence(
                new And(P, Q),
                new Nand(new Nand(P, Q), new Nand(P, Q)));

            TestLogicalEquivalence(
                new Or(P, Q),
                new Nand(new Nand(P, P), new Nand(Q, Q)));

            TestLogicalEquivalence(
                new Xor(P, Q),
                new Nand(
                    new Nand(P, new Nand(P, Q)),
                    new Nand(Q, new Nand(P, Q)))
                );

            Console.ReadKey(true);
        }
    }
}

Код выше написан в идиоматическом стиле C #. Он использует шаблон посетителей, а не типовое тестирование, чтобы гарантировать безопасность типов. Это около 218 LOC.

Вот версия F #:

#light
open System

type expr =
    | True
    | And of expr * expr
    | Nand of expr * expr
    | Or of expr * expr
    | Xor of expr * expr
    | Not of expr

let (^^) p q = not(p && q) && (p || q) // makeshift xor operator

let rec eval = function
    | True          -> true
    | And(e1, e2)   -> eval(e1) && eval(e2)
    | Nand(e1, e2)  -> not(eval(e1) && eval(e2))
    | Or(e1, e2)    -> eval(e1) || eval(e2)
    | Xor(e1, e2)   -> eval(e1) ^^ eval(e2)
    | Not(e1)       -> not(eval(e1))

let rec prettyPrint e =
    let rec loop = function
        | True          -> "True"
        | And(e1, e2)   -> sprintf "(%s) AND (%s)" (loop e1) (loop e2)
        | Nand(e1, e2)  -> sprintf "(%s) NAND (%s)" (loop e1) (loop e2)
        | Or(e1, e2)    -> sprintf "(%s) OR (%s)" (loop e1) (loop e2)
        | Xor(e1, e2)   -> sprintf "(%s) XOR (%s)" (loop e1) (loop e2)
        | Not(e1)       -> sprintf "NOT (%s)" (loop e1)
    (loop e).Replace("(True)", "True")

let testLogicalEquivalence e1 e2 =
    let eval1, eval2 = eval e1, eval e2
    printfn "Testing expressions:"
    printfn "    First  = %s" (prettyPrint e1)
    printfn "        eval(e1): %b" eval1
    printfn "    Second = %s" (prettyPrint e2)
    printfn "        eval(e2): %b" eval2
    printfn "    Equilalent? %b" (eval1 = eval2)
    printfn ""

let p, q = True, Not True
let tests =
    [
        p, q;

        Not(p), Nand(p, p);

        And(p, q),
            Nand(Nand(p, q), Nand(p, q));

        Or(p, q),
            Nand(Nand(p, p), Nand(q, q));

        Xor(p, q),
            Nand(
                    Nand(p, Nand(p, q)),
                    Nand(q, Nand(p, q))
                )
    ]
tests |> Seq.iter (fun (e1, e2) -> testLogicalEquivalence e1 e2)

Console.WriteLine("(press any key)")
Console.ReadKey(true) |> ignore

Это 65 LOC. Поскольку он использует сопоставление с шаблоном, а не с шаблоном посетителя, мы не теряем никакой безопасности типов, и код очень легко читается.

Любой тип обработки символов на порядки проще написать на F #, чем на C #.

[Изменить, чтобы добавить:] О, и сопоставление с образцом не просто замена для шаблона посетителя, оно также позволяет сопоставлять данные с формой . Например, вот функция, которая преобразует Nand в их эквиваленты:

let rec simplify = function
    | Nand(p, q) when p = q -> Not(simplify p)
    | Nand(Nand(p1, q1), Nand(p2, q2))
        when equivalent [p1; p2] && equivalent [q1; q2]
                    -> And(simplify p1, simplify q1)
    | Nand(Nand(p1, p2), Nand(q1, q2))
        when equivalent [p1; p2] && equivalent [q1; q2]
                    -> Or(simplify p1, simplify q1)
    | Nand(Nand(p1, Nand(p2, q1)), Nand(q2, Nand(p3, q3)))
        when equivalent [p1; p2; p3] && equivalent [q1; q2; q3]
                    -> Xor(simplify p1, simplify q1)
    | Nand(p, q) -> Nand(simplify p, simplify q)
    | True          -> True
    | And(p, q)     -> And(simplify p, simplify q)
    | Or(p, q)      -> Or(simplify p, simplify q)
    | Xor(p, q)     -> Xor(simplify p, simplify q)
    | Not(Not p)    -> simplify p
    | Not(p)        -> Not(simplify p)

Невозможно написать этот код кратко на C #.

14 голосов
/ 29 марта 2009

Существует множество примеров, но ни один из них не будет настолько полным, как использование примера, относящегося к одному из ваших проектов на работе. Такие примеры, как «Сумма квадратов» Луки, потрясающие, но если бы кто-то использовал это как доказательство того, как наша кодовая база могла бы быть написана лучше, я бы не был убежден. Все это доказывает, что некоторые вещи лучше написаны функционально. Что вам нужно доказать, так это то, что ваша кодовая база лучше написана функционально

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

9 голосов
/ 30 марта 2009

Задачи для функционального стиля? В любое время у вас есть общий шаблон кодирования и вы хотите уменьшить его. Некоторое время назад я немного написал об использовании C # для функционального стиля, при этом убедившись, что он практичен: Практический функционал C # (стесняюсь ссылаться на свои материалы здесь, но я думаю, что это уместно в этом случае ). Если у вас есть обычное «корпоративное» приложение, показывать, скажем, как хороши выражения в сопоставлении с образцом, не будет слишком убедительно.

Но в реальных приложениях существует МНОЖЕСТВО шаблонов, которые появляются на низком уровне кодирования. Используя функции высшего порядка, вы можете заставить их уйти. Как я показываю в этом наборе постов в блоге, мой любимый пример - шаблон WCF "try-close / finally-abort". Шаблон try / finally-dispose настолько распространен, что превратился в ключевое слово языка: using. То же самое для "замка". Они оба тривиально представлены как функции более высокого порядка, и только потому, что C # изначально не поддерживал их, нам нужны жестко закодированные ключевые слова для их поддержки. (Быстро: отключите ваши блокировочные блоки, чтобы использовать блокировку ReaderWriter. Ой, сначала нам нужно написать функцию более высокого порядка.)

Но, возможно, чтобы убедить, нужно просто взглянуть на Microsoft. Генерики ака параметрический полиморфизм? Это вряд ли ОО, но хорошая функциональная концепция, которая теперь всем нравится. Милый каркас Ninject не будет работать без него. Лямбда? Как деревья выражений, они - то, как LINQ, Fluent NHibernate и т. Д. Получают всю свою силу. Опять же, это не из ОО или императивного программирования. Новая библиотека потоков? Довольно безобразно без замыканий.

Итак, за последнее десятилетие функциональное программирование благословляло такие вещи, как .NET. Основные достижения (такие как дженерики, «LINQ») происходят непосредственно из функциональных языков. Почему бы не осознать, что в этом что-то есть, и больше участвовать в этом? Так я бы сказал это скептикам.

Большая проблема на самом деле заставляет людей совершить прыжок в понимании функций более высокого порядка. Хотя это довольно легко, если вы никогда не видели этого раньше в своей жизни, это может быть шокирующим непостижимым. (Черт, многие считают, что дженерики предназначены только для безопасных типов коллекций, а LINQ - это просто встроенный SQL.)

Итак, что вам нужно сделать, это пройти через свою кодовую базу и найти места, которые являются чрезмерно сложным императивным кошмаром. Ищите базовые шаблоны и используйте функции, чтобы аккуратно связать их вместе. Если вы не можете найти что-либо, вы можете просто демо-списки. Например, «найти все Foos в этом списке и удалить их». Это функциональность в 1 строку в функциональном стиле «myList.Remove (x => x.Bla> 0)» по сравнению с 7 строками в стиле C # (создание временного списка, циклическое переключение и добавление элементов для удаления, создание циклов и удаление элементов ).

Надежда состоит в том, что, хотя синтаксис странный, люди узнают: «Вау, это намного проще». Если они могут на некоторое время записать «многословный == более читабельный» и «выглядящий сбивающим с толку», у вас будет шанс.

Удачи.

2 голосов
/ 31 марта 2009

Лучшей пропагандистской статьей, когда-либо написанной для функционального стиля, является статья Джона Хьюза под названием Почему функциональное программирование имеет значение . Я предлагаю вам сделать несколько примеров для себя, пока вы не достигнете стадии, на которой вы сможете убедительно привести аргументы, изложенные в этом документе.

Многие из приведенных в статье примеров являются числовыми и не соответствуют современной аудитории. Еще одно современное упражнение, которое я дал своим ученикам, заключалось в том, чтобы использовать идеи, изложенные в этой статье, для упаковки больших мультимедийных файлов на диски объемом 4,7 ГБ для резервного копирования. Они использовали алгоритм «пузырькового поиска» Михаэля Митценмахера для создания альтернативных упаковок, и, используя этот алгоритм и методы Хьюза, было легко получить каждый DVD (кроме последнего) заполненным на 99,9%. Очень мило.

2 голосов
/ 30 марта 2009

По существу, функциональная парадигма очень эффективна для параллельной обработки:

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

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

http://www.joelonsoftware.com/items/2006/08/01.html

1 голос
/ 29 марта 2009

Если под «функциональным стилем» вы подразумеваете использование таких понятий, как «карта», «применить», «уменьшить», «фильтровать», лямбда-функции и списки, то должно быть очевидно, что код, который имеет дело с Операции со списками почти всегда более лаконичны при написании в «функциональном стиле». Но если вы смешиваете «функциональный стиль» с императивным кодом на преимущественно императивном языке, это действительно вопрос стиля.

В python, например, вы можете переопределить взломанный Haskell qsort, опубликованный так:

def qsort(list):
    if list == []:
        return []
    else:
        x = list[0]; xs = list[1:]
        return qsort(filter(lambda y: y<x, xs)) + [x] + qsort(filter(lambda y: y>= x, xs))

Хотя последняя строка записывается как

return qsort([y for y in xs if y<x]) + [x] + qsort([y for y in xs if y>=x])

возможно более «питоничен».

Но это, очевидно, более кратко, чем реализация здесь:

http://hetland.org/coding/python/quicksort.html

Кстати, именно так я и подумал бы реализовать это до того, как начал изучать Хаскелл.

Функциональная версия предельно ясна , если и , только если , вы привыкли к функциональному программированию и понимаете, что filter собирается делать так же легко, как и изношенный программист C ++ начинает цикл for, даже не задумываясь об этом. И это ясно, что реальная проблема на карту здесь: функциональное «стилевое» программирование - это совершенно другое мышление . Если люди, с которыми вы работаете, не привыкли мыслить рекурсивно и не из тех, кого волнует, не просто из-за новой технологии, а из совершенно другого способа решения своих проблем, тогда любое количество сравнений кода не требуется. не собираюсь завоевывать их.

1 голос
/ 29 марта 2009

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

Бесполезно использовать пару алгоритмов для демонстрации возможностей функционального программирования, если оно совершенно бесполезно для вашей бизнес-сферы. Итак, возьмите некоторый существующий код и переписайте его функционально. Если вы сможете доказать, что это лучше, люди будут вас слушать - вы показали им конкретный, соответствующий пример. Если вы не можете, то, возможно, функциональное программирование - это не решение, которое вы искали.

1 голос
/ 29 марта 2009

Другим примером может служить алгоритм быстрой сортировки . Это можно очень кратко описать на функциональном языке, таком как Haskell:

qsort []     = []
qsort (x:xs) = qsort (filter (< x) xs) ++ [x] ++ qsort (filter (>= x) xs)

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

0 голосов
/ 26 мая 2015

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

http://blogs.msdn.com/b/ericwhite/archive/2006/10/04/fp-tutorial.aspx

0 голосов
/ 22 декабря 2009

Недавно я придумал небольшую хитрость, чтобы сделать лямбды, передаваемые в мои методы расширения, более похожими на F # иш. Вот оно:

То, что я хотел сделать, было что-то вроде:

3.Times(() => Console.WriteLine("Doin' it"));

Теперь метод расширения для этого легко реализуется:

    public static void Times(this int times, Action action)
    {
        Enumerable.Range(1, times).ToList().ForEach(index => action());
    }

Что мне не понравилось, так это то, что я указываю индекс здесь: ForEach(index => action()), хотя он никогда не используется, поэтому я заменил его на _ и получил ForEach(_ => action())

Это хорошо, но теперь я был мотивирован, чтобы мой код вызова был похож

(мне никогда не нравилось "()" в начале лямбда-выражения), поэтому вместо: 3.Times(() => ...); я хотел 3.Times(_ => ...); Единственный способ реализовать это - передать ложный параметр в метод расширения, который никогда не используется, и поэтому я изменил его так:

    public static void Times(this int times, Action<byte> action)
    {
        Enumerable.Range(1, times).ToList().ForEach(_ => action(byte.MinValue));
    }

Это позволяет мне называть это так:

3.Times(_ => Console.WriteLine("Doin' it"));

Не так много изменений, но я все еще наслаждался тем, что можно было так легко сделать эту небольшую подстройку и в то же время убрать шум "()", что делает его намного более читабельным.

...