Дмитpий Hecтepук

Блог о программировании — C#, F#, C++, архитектура, и многое другое

Уравнения на F# проще чем на C#?

22 комментария

На недавней встрече, посвещенной языку F#, я показал как якобы «элегантно» можно описывать математические функции на F#. А сейчас сел и задумался – так ли это на самом деле? Давайте разберемся.

Квадратное уравнение

Начнем с примера, приведенного мною на встрече – решения квадратного уравнения. Казалось бы – что может быть проще?

Сразу можно сделать несколько сравнений C# и F# для этого примера:

  • Возвращаются 2 значения, а следовательно F# выигрывает за счет того что не надо писать Tuple.Create, а тому кто эти значения получит не надо использовать абсолютно ничего не значащие свойства Item1 и Item2
  • Математические binding’и у F# намного «ближе» чем у C#, а следовательно в этом случае (да и в общем), ваш код на F# не будет испачкан явным вызовом статических функций вроде Math.Sqrt. Меня всегда это бесило в C# т.к. сложные вычисления становится еще сложнее читать.
  • Ни C# ни F# не умеют работать с оператором ±. Но если в C# нам придется тупо вызывать вычисления дважды, в F# мы можем просто определить функцию конечного вычисления, а потом передать в нее операторы (+) и (-).

В результате получим следующее сравнение:

C#

public Tuple<double,double> SolveQuadraticEquation(double a, double b, double c)
{
  double discRoot = Math.Sqrt(b*b-4.0*a*c);
  double x1 = (-b + discRoot) / (2 * a);
  double x2 = (-b - discRoot) / (2 * a);
  return Tuple.Create(x1, x2);
}

F#

let solveQuadratic a b c =
  let disc = b * b - 4.0 * a *c
  let calc op = (op (-b) (sqrt disc)) / (2.0*a)
  (calc (+), calc(-))

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

C#

public Tuple<double, double> SolveQuadraticEquation(double a, double b, double c)
{
  double q = -0.5 * (b + Math.Sign(b) * Math.Sqrt(b * b - 4 * a * c));
  return Tuple.Create(q / a, c / q);
}

F#

let solveQuadraticEquation a b c =
  let q = -0.5 * (b + (sign b |> float) * sqrt(b * b - 4.0 * a * c))
  (q / a, c / q) //           ^^^^^^^^ WTF?!?

Пример на F# должен вызвать у вас возмущение – с какой это радости результат (sign b) должен быть приведен к типу float?1 А дело все в непродуманности API, согласно которому sign(x) – это всегда int (могли бы сделать тем же типом что и аргумент), а также крайне жесткой системе типов в которой float + int * float не воспринимается и никак не вычисляется.

Такое странное поведение F# унаследовано из OCaml и требуется для правильного вывода типов. Если вам действительно интересны причины – можно почитать тут. Мне же это лишь палки в колеса. Да, и я знаю что можно было бы написать sign(float b) или что-то в этом роде. Тут как бы без разницы, все равно «не очень».

Комплексные решения

Ну да ладно, а вот вопрос – что будет если дискриминанта (Δ) меньше нуля? С одной стороны, и C# и F# поддерживает комплексные типы данных (см. System.Numerics). Если сделать вид что мы все будем передавать как комплексные числа (так проще?), то получим вот такой вот код:

C#

public Tuple<Complex, Complex> SolveQuadraticEquation3(double a, double b, double c)
{
  double det = b * b - 4 * a * c;
  double absRoot = Math.Sqrt(Math.Abs(det));
  Complex root = det < 0 ? new Complex(0, absRoot) : new Complex(absRoot, 0);
  Complex q = -0.5 * (b + Math.Sign(b) * root);
  return Tuple.Create(q / a, c / q);
}

F#

let solveQuadratic3 a b c =
  let complex v = Complex(v, 0.0)
  let det = b * b - 4.0 * a * c
  let absRoot = sqrt(det |> abs)
  let root = if det < 0.0 then Complex(0.0, absRoot)
                          else Complex(absRoot, 0.0)
  let q = -(complex 0.5) * ((complex b) + (sign b |> float |> complex) * root)
  (q / complex(a), complex(c) / q)

Эээ, ну что я могу сказать? F# определенно превносит массу головной боли. Проблема тут как раз в том, что в C# нам доступны операторы автоконверсии вроде floatComplex, но в F# они попросту не работают! Поэтому вы можете либо определить их как функции (благо у них есть имя op_Implicit), либо же просто вызвать конструктор как делаю я.2

Заключение

То ли я чего-то недопонимаю, то ли F# действительно неудобен для работы с математикой? Ведь если нужно каждый int кастовать во float, а каждый float в Complex, и так далее, то это же сколько лишнего, никому не нужного и мешающего восприятию кода надо написать?

Критика welcome! А то может я что не так написал?

Заметки

  1. Напоминаю, что float в F# – это double в C#. А C#-ный float в F# называется float32.
  2. Тут еще хочется заметить что if который выбирает какой Complex создавать как бы не нужно — можно было вместо этого написать Complex.Sqrt(new Complex(b*b-4*a*c, 0.0)). Проблема только в том что в результате этого вычисления Real-значение будет чуточку отличаться от нуля. Например sqrt(Complex(-4.0, 0.0)) равен (1.22460635382238E-16, 2).
Advertisements

Written by Dmitri

5 февраля 2011 в 10:52

Опубликовано в .NET, C#, f#

комментария 22

Subscribe to comments with RSS.

  1. Записывать математические выражения на языках программирования в принципе неудобно.

    muradovm

    5 февраля 2011 at 12:10

    • Да, но писать-то надо. Правда есть всяческие ухищрения.

      Dmitri

      5 февраля 2011 at 12:24

      • Ухищрений много. Например можно использовать MathExpressionEditorLight, там все проще..

        Toha

        5 февраля 2011 at 15:30

        • А он позволяет формулы конвертировать в C#?

          Dmitri

          5 февраля 2011 at 15:55

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

          Toha

          5 февраля 2011 at 17:37

  2. […] This post was mentioned on Twitter by Metavirus Ok and Sergey Gavruk, Newsforanton. Newsforanton said: Уравнения на F# проще чем на C#?: На недавней встрече, посвещенной языку F#, я показал как якобы «элегантно» мож… http://bit.ly/dVaBXA […]

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

    Юра

    5 февраля 2011 at 13:13

    • Ну я все понимаю. Просто за приведение типов приходится платить. Я в первый раз на это наткнулся когда работал с System.Xml.Linq где строки нельзя было автопривести к XName.

      Dmitri

      5 февраля 2011 at 15:54

    • Я бы сказал, что проблема не в выводе типов, а в кривых функциях.
      В хаскеле например определение знака дано так:
      signum :: Num a => a -> a
      Что позволяет избавиться от кастования инта к флоату.

      Опять же квадратный корень определён как
      sqrt :: (Floating a) => a -> a
      и возвращает значение того-же типа.

      соответвенно код
      solveQuadratic a b c = let q = -0.5*(b + signum b * sqrt (b*b-4*a*c )) in (q/a,c/q)

      будет работать и для простых чисел:

      Prelude Data.Complex> solveQuadratic 1 2 0
      (-2.0,-0.0)

      и для комлексных
      Prelude Data.Complex> slv (1:+0) (2:+0) (3:+0)
      ((-1.0) :+ (-1.4142135623730951),(-0.9999999999999999) :+ 1.414213562373095)

      К сожалению от проблем погрешностей не уйти.
      Но ограничения .Net плафтормы не дают возможности использовать подобные конструкции.

      Steck

      18 февраля 2011 at 13:00

      • тьфу, запутал.
        slv и solveQuadratic у меня одно и тоже. Просто переименовал чтобы больше соответсвовало наименованию, а скопировал из хистори.

        Prelude Data.Complex> solveQuadratic (1:+0) (2:+0) (3:+0)
        ((-1.0) :+ (-1.4142135623730951),(-0.9999999999999999) :+ 1.414213562373095)
        естественно вот так, и работает.

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

        Steck

        18 февраля 2011 at 13:02

        • Н-да, однако. И все же тут, как говориться, .Net по полезности перевешивает монадичность и «чистоту» Haskell. Хотя на вкус и цвет…

          Dmitri

          20 февраля 2011 at 20:37

  4. Можно долго спорить — что проще, в любом случае — результат субъективен.
    Очень интересная статья.

    Toha

    5 февраля 2011 at 17:41

  5. Если в названии говорится про уравнения, то я ожидал увидеть запись уравнения a*x*x + b*x + c = 0, а не выражения для вычисления корней уравнения. Может я не прав.

    Unk

    5 февраля 2011 at 19:13

  6. Разумеется, красота требует жертв! Типы тоже требуют жертв. Если вас не интересуют типы, Scheme — для вас. Для данной задачи (писать красивый код для численных задач) я серьёзно рассмотрел бы этот язык, особенно учитывая встроенную поддержку комплексных чисел:

    Anton Tayanovskyy

    5 февраля 2011 at 19:48

    • Работая с System.Linq и вообще в F#, всегда можно сделать жизнь проще и веселее маленьким оператором:

      Anton Tayanovskyy

      5 февраля 2011 at 19:50

      • Опа, а вот насчет этого вообще не думал — использовал какой-то строковый литерал :)

        Dmitri

        5 февраля 2011 at 19:53

      • Согласен с Антоном, при помощи своего оператора тут можно сделать запись короче, а с inline еще и избавится от ненужного приведения

        Vladimir

        7 февраля 2011 at 18:51

    • Да, но это же нечитабельно! :) К тому же, я как бы констатирую тот факт что банки используют F# для деривативов, следовательно они мирятся со всеми проблемами, а может даже и выигрывают от такой «строгости».

      Кстати: спасибо за очередной релиз. Независимые HTML-сайты это именно то что было нужно. Теперь кажется можно начинать работать…

      Dmitri

      5 февраля 2011 at 19:52

      • Я по сути согласен — и лично я рад строгости даже если это означает лишний оператор тут и там. А всё же Scheme язык красивый, и очень даже читаемый после 10 минут адаптации глаз — правил в синтаксисе меньше, чем в C#/F#. Сравнивать мой код нужно с последним вашим решением, так как он комлексные корни находит без проблем!

        RE: WebSharper — очень рад буду фидбэку. Независимые HTML-сайты — это начало. Предстоит работа по серверной части (RPC-хост) на Mono, возможно FastCGI.. И еще: согласно моему боссу у ранних пользователей большой шанс на преференции в получении лицензий.

        Anton Tayanovskyy

        6 февраля 2011 at 0:20

      • Просто там квонты-матлабщики, для них не функциональное программирование это нечто что надо жечь священным огнем.

        Vitaly

        7 февраля 2011 at 15:31

  7. За мощный вывод типов надо платить. За строгую типизацию тоже надо платить.

    В семействе ML-языков никогда не было неявных приведений типов, что позволяет использовать в языке мощный вывод типов. Да, F# требует больше писанины в самых неожиданных для C#-программиста местах, но это общий подход языка: все приведения типов — явные. Вы смотрите на это лишь через призму количества символов, а можно посмотреть со стороны проверки типов и корректности выражения.

    Вообще говоря, абсолютно большинство чисел типа int невозможно точно представить в виде значения типа float (в виде двоичной дроби). Фактически 4 и 4.0 — это разные значения.

    В OCaml вообще операторы для float-арифметики были отдельные: +. -. /. *.

    sqrt(det |> abs) — ну зачем тут пайп? sqrt(abs det)

    Если вам нужен sign, возвращающий float, то напишите свой signf. Неужели это сложнее, чем пихать везде преобразование типа и потом ныть почему же так распухает код?

    ControlFlow

    6 февраля 2011 at 16:16

  8. Привет Дмитрий!
    Я не тормоз, просто только сейчас прочитал статью :)
    В инженерных и финансовых расчетах 90% — double, float или decimal. Так что все равно на F# алгоритмы реализовывать легче, чем на C#.

    michael

    26 сентября 2012 at 16:17


Оставить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s

%d такие блоггеры, как: