Уравнения на F# проще чем на C#?
На недавней встрече, посвещенной языку 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# нам доступны операторы автоконверсии вроде float→Complex, но в F# они попросту не работают! Поэтому вы можете либо определить их как функции (благо у них есть имя op_Implicit), либо же просто вызвать конструктор как делаю я.2
Заключение
То ли я чего-то недопонимаю, то ли F# действительно неудобен для работы с математикой? Ведь если нужно каждый int кастовать во float, а каждый float в Complex, и так далее, то это же сколько лишнего, никому не нужного и мешающего восприятию кода надо написать?
Критика welcome! А то может я что не так написал?
Заметки
- ↑ Напоминаю, что
floatв F# – этоdoubleв C#. А C#-ныйfloatв F# называетсяfloat32. - ↑ Тут еще хочется заметить что
ifкоторый выбирает какойComplexсоздавать как бы не нужно — можно было вместо этого написатьComplex.Sqrt(new Complex(b*b-4*a*c, 0.0)). Проблема только в том что в результате этого вычисления Real-значение будет чуточку отличаться от нуля. Напримерsqrt(Complex(-4.0, 0.0))равен(1.22460635382238E-16, 2).

Записывать математические выражения на языках программирования в принципе неудобно.
muradovm
5 Февраль 2011 в 12:10
Да, но писать-то надо. Правда есть всяческие ухищрения.
Dmitri
5 Февраль 2011 в 12:24
Ухищрений много. Например можно использовать MathExpressionEditorLight, там все проще..
Toha
5 Февраль 2011 в 15:30
А он позволяет формулы конвертировать в C#?
Dmitri
5 Февраль 2011 в 15:55
Да. Но код будет использовать встроенную библиотеку. А это медленнее чем чистый C#. Зато можно редактировать формулу на уровне пользователя приложения. В общем, везде есть свои преимущества и недостатки.
Toha
5 Февраль 2011 в 17:37
[...] This post was mentioned on Twitter by Metavirus Ok and Sergey Gavruk, Newsforanton. Newsforanton said: Уравнения на F# проще чем на C#?: На недавней встрече, посвещенной языку F#, я показал как якобы «элегантно» мож… http://bit.ly/dVaBXA [...]
Tweets that mention Уравнения на F# проще чем на C#? « Дмитрий Нестерук -- Topsy.com
5 Февраль 2011 в 12:28
Как Вы себе представляете автоматический вывод типов при наличии неявного их приведения? Что-то мне подсказывает, что это либо невозможно, либо слишком сложная вычислительная задача для компилятора.
Юра
5 Февраль 2011 в 13:13
Ну я все понимаю. Просто за приведение типов приходится платить. Я в первый раз на это наткнулся когда работал с System.Xml.Linq где строки нельзя было автопривести к XName.
Dmitri
5 Февраль 2011 в 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 в 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 в 13:02
Н-да, однако. И все же тут, как говориться, .Net по полезности перевешивает монадичность и “чистоту” Haskell. Хотя на вкус и цвет…
Dmitri
20 Февраль 2011 в 20:37
Можно долго спорить – что проще, в любом случае – результат субъективен.
Очень интересная статья.
Toha
5 Февраль 2011 в 17:41
Если в названии говорится про уравнения, то я ожидал увидеть запись уравнения a*x*x + b*x + c = 0, а не выражения для вычисления корней уравнения. Может я не прав.
Unk
5 Февраль 2011 в 19:13
Разумеется, красота требует жертв! Типы тоже требуют жертв. Если вас не интересуют типы, Scheme – для вас. Для данной задачи (писать красивый код для численных задач) я серьёзно рассмотрел бы этот язык, особенно учитывая встроенную поддержку комплексных чисел:
https://gist.github.com/812578
Anton Tayanovskyy
5 Февраль 2011 в 19:48
Работая с System.Linq и вообще в F#, всегда можно сделать жизнь проще и веселее маленьким оператором:
https://gist.github.com/5c10d3b36e91e398deb8
Anton Tayanovskyy
5 Февраль 2011 в 19:50
Опа, а вот насчет этого вообще не думал – использовал какой-то строковый литерал :)
Dmitri
5 Февраль 2011 в 19:53
Согласен с Антоном, при помощи своего оператора тут можно сделать запись короче, а с inline еще и избавится от ненужного приведения
https://gist.github.com/814567
Vladimir
7 Февраль 2011 в 18:51
Да, но это же нечитабельно! :) К тому же, я как бы констатирую тот факт что банки используют F# для деривативов, следовательно они мирятся со всеми проблемами, а может даже и выигрывают от такой “строгости”.
Кстати: спасибо за очередной релиз. Независимые HTML-сайты это именно то что было нужно. Теперь кажется можно начинать работать…
Dmitri
5 Февраль 2011 в 19:52
Я по сути согласен – и лично я рад строгости даже если это означает лишний оператор тут и там. А всё же Scheme язык красивый, и очень даже читаемый после 10 минут адаптации глаз – правил в синтаксисе меньше, чем в C#/F#. Сравнивать мой код нужно с последним вашим решением, так как он комлексные корни находит без проблем!
RE: WebSharper – очень рад буду фидбэку. Независимые HTML-сайты – это начало. Предстоит работа по серверной части (RPC-хост) на Mono, возможно FastCGI.. И еще: согласно моему боссу у ранних пользователей большой шанс на преференции в получении лицензий.
Anton Tayanovskyy
6 Февраль 2011 в 0:20
Просто там квонты-матлабщики, для них не функциональное программирование это нечто что надо жечь священным огнем.
Vitaly
7 Февраль 2011 в 15:31
За мощный вывод типов надо платить. За строгую типизацию тоже надо платить.
В семействе ML-языков никогда не было неявных приведений типов, что позволяет использовать в языке мощный вывод типов. Да, F# требует больше писанины в самых неожиданных для C#-программиста местах, но это общий подход языка: все приведения типов – явные. Вы смотрите на это лишь через призму количества символов, а можно посмотреть со стороны проверки типов и корректности выражения.
Вообще говоря, абсолютно большинство чисел типа int невозможно точно представить в виде значения типа float (в виде двоичной дроби). Фактически 4 и 4.0 – это разные значения.
В OCaml вообще операторы для float-арифметики были отдельные: +. -. /. *.
sqrt(det |> abs) – ну зачем тут пайп? sqrt(abs det)
Если вам нужен sign, возвращающий float, то напишите свой signf. Неужели это сложнее, чем пихать везде преобразование типа и потом ныть почему же так распухает код?
ControlFlow
6 Февраль 2011 в 16:16