Товарищ (RedChrom) задал вопрос, что я использую больше при разработке на Окамле. Не особо задумываясь ответил, что фифти-фифти. Потом сделал простой grep на свои исходники, и выяснил, что на 80% всё-таки модули и функторы. Причём, объекты и классы по большей части в очень старых исходниках. Сейчас 100% функторы.
Сейчас развлекаюсь написанием универсального интерфейса к разным хранилищам (в первую очередь, это FS, memcached, memcachedb) с записями вида key+value. В качестве основы, создал универсальный интерфейс:
module type STORAGE = sig type t type storage_descr_t val init: storage_descr_t -> t init_result val get: t -> Key.t -> get_result Lwt.t val get_list: t -> Key.t list -> get_result Lwt.t list val delete: t -> Key.t -> delete_result Lwt.t val set: t -> Key.t -> ?value_size:value_size -> value -> set_result Lwt.t val add: t -> Key.t -> ?value_size:value_size -> value -> add_result Lwt.t val replace: t -> Key.t -> ?value_size:value_size -> value -> replace_result Lwt.t end
Написал модуль FileSystem, реализующий такой интерфейс. Поскольку файлы могут быть (условно) бесконечного размера, это стало самой простой реализацией интерфейса. Но не всё в жизни так легко. Например, memcached имеет максимальный размер пары key+value где-то в районе 64KB. А хочется в нём хранить побольше. Да и файловые системы бывают дурные. Напрмер, не позволяют создавать файлы больше 512MB. Поэтому на основе FileSystem (ключевое слово import) был написан маленький модуль FileSystemSized, который помимо value пишет в файлик размер value. Штука, на первый взгляд, бесполезная. Но идём дальше. Создаём функтор Splitted, который принимает модуль с интерфейсом STORAGE и делает модули с тем же уже знакомым нам интерфейсом STORAGE. Этот функтор хитрый: он знает, какого максимум размера value может хранить переданный ему модуль. Если в одну запись не влазит — он бьёт value на кусочки, для каждого кусочка генерит свой уникальный ключ, и таким образом пишет. В первом кусочке он сохраняет реальный размер всего value. Вот зачем нам нужен был модуль FileSystemSized. На основе этого функтора получается чудесный модуль FileSystemSplitted. По образу и подобию можно сделать MemcachedSplitted. Или даже FileSystemSplitted_and_again_Splitted, только я не придумал зачем :)
Мыслим вперёд. Ключ у нас — строчка строгой длины, с равной вероятностью имеющая в первом/втором/третьем/… байте любое из значений 0..255. На основе первого байта ключа, мы можем раскидать пары key+value на 256 физических хранилищ (ну, или меньше, как захочется). Назовём такой функтор Distributed, и он тоже будет создавать модули с интерфейсом STORAGE. Получается изумительная картинка:
(* Создаём уже знакомый нам модуль для хранения больших значений в маленьких файлах. *) module FileSystemSplitted = Splitted(FileSystemSized) (* Каждое значение хранится в одном файлике, но файлики раскидываются по разным директориям. *) module FileSystemDistributed = Distributed(FileSystemSized) (* Каждое значение пилится на части, части раскидываются по разным директориям. С remote mount, получаем хранилище бесконечного размера для почти бесконечного количества значений. *) module FileSystemDistributedSplitted = Splitted(FileSystemDistributed) (* Каждое значение хранится в одной директории, но там оно пилится на части. Директорий для хранения может быть несколько. Хранилище получается тоже бесконечным, но наполняется не так равномерно. *) module FileSystemSplittedDistributed = Distributed(FileSystemSplitted)
Как видите, с помощью фактически одного интерфейса и двух функторов, получаем невероятное, лютое количество вариантов хранилища. Даже круче, чем в GlusterFS или GNU Hurd. А ведь мысль можно продолжить ещё дальше, дописав функторы Raid0 (писать значение в несколько хранилищ), Clustered (создаётся на основе списка хранилищ, умеющих только читать и списка хранилищ, умеющих изменять), Flock (за счёт некоторого performace degradation, делать exclusive lock на записи)… чем я и занимаюсь :) Замечу, что всё это в функционально чисто, без side-effects (они есть только на уровне хранилища) и безумно типобезопасно.
#1 by RedChrom on October 18th, 2009
Ну кагбе факт, что функторы хороши для интерфейсов, а вот когда нужен ad-hoc полиморфизм, всё-таки классы лучше :) У Лероя была где-то презентация на эту тему, где он описывает почему объекты используются только в единственном месте компилятора и почему они вписались туда лучше всего.
Тем не менее, интерфейсы нужны чаще и функторы с этим хорошо справляются. Правда получилось как-то так, что я брал конструктор из параметра функтора и внезапно оказалась, что одна из реализации интерфейса зависит от других данных и у меня есть два пути решить эту проблему: сделать ещё один функтор и следовательно прослойку (привет костыли из мира C++) либо просто передавать конструктор по другому :)
П.С. А typeclasses я всё равно хочу.