Pattern-matching FTW!
Все никак не привыкну мыслить целиком в функциональном стиле, с паттерн-матчингами и прочими ништяками.
В одном моем проектике (который эти самые пресловутые ГА) нужно было представлять математические выражения деревьями и всю работу с этими выражениями делать через эти деревья. Соответственно, был статичный словарь ([(a, b)]) идентификаторов функций и, собственно, самих функций. Вернее, два таких словаря, для унарных и бинарных функций. Что-то типа такого:
data UnaryFunc = Sin | Cos | Log | Tan | Asin | Acos | Atan data BinaryFunc = Plus | Minus | Mul | Div | Pow binaryOps = [ (Plus, (+)), (Minus, (-)), (Mul, (*)), (Div, (/)), (Pow, (**)) ]
Соответственно, кусок функции вычисления значения дерева в точке, обрабатывающий вершины с функциями, выглядел как-то так:
Страшно и тупо, правда? Страшнее становится, если запустить программу под профилером и посмотреть, что жрет больше всего процессора и больше всего грузит GC. Победителями оказываются, собственно:
COST CENTRE MODULE %time %alloc evalTree ExprTree 47.7 13.6 binaryOps Funcs 17.2 47.9
Это собственные значения, без учета вызываемых в случае evalTree подфункций. И тут даже и близко нет ни операций над матрицами, ни вычислений якобиана, ни символьного дифференцирования. Ужас-ужас, короче.
Но тут стоит посмотреть, что список функций-то статичный, и, по факту, стоит вспомнить про паттерн-матчинг. Например, binaryFuncs перепишется следующим образом:
binaryOps :: Floating a => BinaryFunc -> a -> a -> a binaryOps Plus = (+) binaryOps Minus = (-) binaryOps Mul = (*) binaryOps Div = (/) binaryOps Pow = (**)
Функция, вычисляющая значение дерева, в свою очередь будет выглядеть так:
evalTree _ (LC c) = c evalTree vars (NUn f t) = unaryOps f $ evalTree vars t evalTree vars (NBin f l r) = binaryOps f (evalTree vars l) (evalTree vars r)
Одна эта мелкая оптимизация ускоряет выполнение раза в два-три и обеспечивает существенно меньшее GC time, что существенно, так как алгоритм отлично параллелится, и
убивать возможность распараллеливания из-за GC не очень бы хотелось.
В результате, правда, binaryOps все еще жрет 20% процессора и ответственна за треть аллокаций. Однако, стоит иметь ввиду такой очевидный факт, что компиляция с -prof -auto-all существенно меняет то, как исполняется код, и отменяет часть оптимизаций, поэтому ориентироваться на конкретные цифры бессмысленно. Главное — что скорость без профилирования существенно повыше стала.
В принципе, если бы не нужно было бы Ord и Eq от деревьев, то можно было бы хранить сами функции в вершинах вместо их идентификаторов.
С другой стороны, новые деревья у меня порождаются сравнительно редко, а вот значения вычисляются часто, в среднем тысячу раз на одно дерево, поэтому разумной оптимизацией может оказаться однократная подстановка функций в дерево. Этакий вариант типа-как-компиляции.
Правда, для этого придется еще пообмазываться type families всякими, ибо хранить уже скомпилированное дерево в том же ExprTree не получится, а генетические алгоритмы у меня работают не напрямую с деревьями, а с инстансами тайпкласса GAble a, поэтому нужно будет запилить что-то типа
class (...) => GAble a where type Compiled a :: * -> *