illumium.org

Главная › Блоги › Блог kayo

И снова декларативное разграничение доступа на Haskell

kayo — Вс, 14/09/2014 - 23:54

Казалось бы, мы уже окончательно пережевали эту тему в прошлый раз. Но оказывается, как всегда, мы всё сделали не правильно… Ну ладно, не будем отчаиваться, мы не совсем всё сделали не правильно. Не правильно мы сделали только в одной части, а именно, в части объявления новых операторов.

Нет, конечно, возможность объявлять хитрые специфические операторы — одна из полезнейших возможностей Haskell, и нет ничего плохого в том, чтобы так поступать. Плохо другое, объявив операторы &&@ и ||@, мы по сути продублировали функциональность. Булеву алгебру не мы изобрели, стандартные операторы && и || давно существуют и делают понятные вещи. Так зачем же снова изобретать велосипед, когда правильнее было бы адаптировать то, что уже есть.

Делаем правильно

Классы типов

В языке Haskell есть мощная концепция, которая называется классами типов. Эта концепция связана с ООП, но это не ООП в классическом виде. Я хочу сказать, что это лучше, чем ООП: это просто, понятно и удобно. Классы в Haskell — это сущности, объединяющие типы в группы, по применимости к ним некоторого набора операций. Так, например, стандартный класс Num объединяет все числовые типы тем, что к ним применимы операции +, –, ×. В общем всё то, что вы привыкли делать с числами, вы можете делать с типами класса Num, будь то Int или Float или другой числовой тип, не важно, вы всегда получите ожидаемый результат.

Предположим, для финансовых вычислений мы захотели реализовать тип DecFloat, который реализует числа с плавающей запятой, представленные в десятичном, а не двоичном виде, как стандарный тип Float. Всё, что нам нужно, чтобы наш тип стал на самом деле числовым типом, — это реализовать экземпляр класса Num. Экземпляр класса подразумевает реализацию базового набора операций для нашего типа. Мы знаем, как устроен тип DecFloat, поэтому для нас не будет большой проблемой реализовать сложение, вычитание, умножение и т.д. А на весь остальной код финансовых вычислений никак не повлияет выбор конкретного типа чисел, который мы хотели бы использовать.

Иногда концепцию классов типов отождествляют с понятием интерфейсов (или виртуальных классов) из ООП. Так класс — это интерфейс, а экземпляр класса — это реализация интерфейса. На мой субъективный взгляд, принятая в Haskell терминология лучше отражает суть. Класс — это именно класс, как нечто более абстрактное, чем тип, который является чем-то вполне определённым и конкретным, в отличии от класса, который относится ко многим типам.

В ООП же класс — это на самом деле тип, то есть вполне конкретная вещь с конкретным внутренним наполнением. Вся эта затейливая иерархия наследования — как раз про то, как писать код в виде набора патчей, применяющихся ко вполне конкретным осязаемым вещам, чтобы превращать их в другие конкретные осязаемые вещи. И в этой иерархии патчей надо уметь удачно маневрировать, обходя грабли, и вешать предупреждающие таблички, чтобы другие не наступили на них. По проектированию софта на объектно-ориентированных языках написано много толстых книг, которые необходимо читать, чтобы владеть всеми удачными паттернами, избегая неудачных.

Я полагаю, что наследование — вот что плохо в ООП. Но это основа, если её убрать, то концепция ООП рассыпется. Но если всё же её убрать, то есть запретить наследование реализации, оставив только инкапсуляцию. Интерфейсы заменить классами, а реализацию интерфейсов заменить экземплярами классов. Типы и классы сделать полностью независимыми сущностями, связанными только через экземпляры. На практике это работает куда лучше, чем представляется в теории.

Как всё обстоит на самом деле

В текущей реализации Prelude нет класса Boolean, который мы бы хотели реализовать для нашего типа AccessRule, чтобы применять к нему обычные операции булевой алгебры. Но это не проблема, потому что в репозитории пакетов Hackage нашелся пакет cond, где умные дяди уже всё сделали за нас. В этом пакете определён интересующий нас класс Boolean и его реализация для стандартного типа Bool. Стоит заглянуть в исходник модуля, внутри всё достаточно просто и очевидно. Чтобы не возникло конфликта между операторами булевой алгебры с определениями из Prelude, нужно спрятать последние через hiding.

Поскольку наш тип AccessRule на самом деле синоним типа Reader User Bool, не удастся так просто определить экземпляр класса Boolean для него. В этом нам помогут расширения FlexibleInstances и GADTs, которые в данном случае можно просто включить и радоваться тому, что всё работает ^_^

Делаем правильно наконец таки

Убираем из кода определения операторов &&@ и ||@, то есть всё вот это:

infixr 4 &&@
infixr 3 ||@

(&&@), (||@) :: (Functor m, Applicative m) =>
                m Bool -> m Bool -> m Bool

leftRule &&@ rightRule = (&&) <$> leftRule <*> rightRule
leftRule ||@ rightRule = (||) <$> leftRule <*> rightRule

И добавляем вот что:

instance Boolean AccessRule where
  true = return True
  false = return False
  --not x = not <$> x
  not = (not <$>)
  a && b = (&&) <$> a <*> b
  a || b = (||) <$> a <*> b

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

На самом деле, как вы уже, наверно, догадываетесь, необходимо реализовать только true или false, not и && или || и это действительно так. Но в таком случае будут использоваться не очень эффективные реализации, выражающие недостающие операции через уже имеющиеся. Реализовывать необходимый и достаточный минимум удобно для быстрого получения рабочего кода, но затем желательно реализовать все операторы из соображений вычислительной эффективности.

В качестве развлечения мы можем реализовать операторы и вовсе без использования <$> и <*>. Это возможно, поскольку Reader это монада и поэтому с ним можно работать как с монадами:

instance Boolean AccessRule where
  true = return True
  false = return False
  -- not a = do
  --   x <- a
  --   return $ not x
  not = (>>= return . not)
  a && b = do
    x <- a
    y <- b
    return $ x && y
  a || b = do
    x <- a
    y <- b
    return $ x || y

Здесь я тоже закомментировал более многословный вариант оператора not, использовав частичное применение. Вместо поднятия операторов в монаду, здесь извлекаются значения и производятся операции над ними, возвращая результат обратно в монаду. Нотация do — это лишь синтаксический сахар для последовательного применения операторов >> и >>= к монадическим значениям. Вот как это будет выглядеть без do:

instance Boolean AccessRule where
  true = return True
  false = return False
  not a = a >>= \x ->
                 return $ not x
  a && b = (a >>= \x ->
                   b >>= \y ->
                          return $ x && y)
  a || b = (a >>= \x ->
                   b >>= \y ->
                          return $ x || y)

Согласитесь, не очень.

 

Осталось правильно импортировать модули:

import Prelude hiding ((&&), (||), not)
import Data.Algebra.Boolean

Теперь можно переписать наши функции проверки доступа привычным и понятным образом без использования хитрых операторов:

--canView :: Memo -> AccessRule
canView memo = hasPerm ViewAnyMemo
               || hasPerm ViewOwnMemo && isOwner memo
               || hasPerm ViewPubMemo && isPublic memo

--canEdit :: Memo -> AccessRule
canEdit memo = hasPerm EditAnyMemo
               || hasPerm EditOwnMemo && isOwner memo

Я вас не обманываю, это всё, что мы сделали не правильно с первого раза и исправили. Такая огромная принципиальная разница в подходах и такие незначительные изменения в коде.

Собираем, запускаем и радостно хлопаем в ладоши

Для сборки проектов на Haskell удобно использовать Cabal, посему на этот раз я обернул пример в проект. Пробовать так:

cd path/to/example
cabal sandbox init
cabal install --dependencies-only
cabal configure
cabal build
cabal run

Результат в точности тот же, что и раньше. Для удобства я оформил пример в виде git репозитория, где есть коммит с первоначальной версией и с исправленной. Результат приложен к статье.

Вот и всё, надеюсь, было весело ^_^

ВложениеРазмер
AccessExample.tar.xz12.88 КБ
  • Разработка для WEB
  • Haskell
  • Monad
  • role-based access control
  • type classes
  • классы типов
  • монады
  • операторы
  • управление доступом
  • Бортовой журнал Иллюмиума

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

Содержимое этого поля является приватным и не будет отображаться публично.
  • Доступные HTML теги: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Syntax highlight code surrounded by the {syntaxhighlighter SPEC}...{/syntaxhighlighter} tags, where SPEC is a Syntaxhighlighter options string or "class="OPTIONS" title="the title".

Подробнее о форматировании

CAPTCHA
Этот вопрос задается для того, чтобы выяснить, являетесь ли Вы человеком или представляете из себя автоматическую спам-рассылку.
  ____     ___    __  __   _____  __     __        
| _ \ / _ \ | \/ | |___ | \ \ / / _ __
| |_) | | (_) | | |\/| | / / \ \ / / | '_ \
| __/ \__, | | | | | / / \ V / | |_) |
|_| /_/ |_| |_| /_/ \_/ | .__/
|_|
Введите код, изображенный в стиле ASCII-арт.
RSS-материал

Навигация

  • Подшивки
  • Фотоальбомы

«Иллюмиум» на якоре.

Работает на Drupal, система с открытым исходным кодом.

(L) 2010, Illumium.Org. All rights reversed ^_~