Метод набора классов в Haskell с использованием State-Monad - PullRequest
0 голосов
/ 01 июня 2011

Я недавно взглянул на Монаду Хаскелла - Государство. Мне удалось создать функции, которые работают с этой монадой, но я пытаюсь инкапсулировать поведение в класс, в основном я пытаюсь воспроизвести в Haskell что-то вроде этого:

class A {
  public:
  int x;
  void setX(int newX) {
      x = newX;
  }
  void getX() {
      return x;
  }
}

Буду очень признателен, если кто-нибудь сможет помочь с этим. Спасибо!

Ответы [ 3 ]

7 голосов
/ 01 июня 2011

Я бы начал с того, что заметил, что Haskell, если не сказать больше, не поощряет развитие в традиционном ОО-стиле;вместо этого он обладает функциями и характеристиками, которые хорошо подходят для «чисто функциональных» манипуляций, которые вы не найдете на многих других языках;Суть в том, что попытка «перенести» понятия из других (традиционных языков) часто может быть очень плохой идеей.

, но я пытаюсь инкапсулировать поведение в класс

Следовательно, мой первый главный вопрос, который приходит на ум, - почему?Конечно, вы должны захотеть сделать что-то с этим (традиционным OO-понятием a) классом?

Если приблизительный ответ на этот вопрос: «Я хотел бы смоделировать какие-то данныеконструируйте ", тогда вам лучше работать с чем-то вроде

data A = A { xval :: Int }

> let obj1 = A 5
> xval obj1
5
> let obj2 = obj1 { xval = 10 }
> xval obj2
10

, который демонстрирует чистые неизменные структуры данных, наряду с функциями 'getter' и деструктивными обновлениями (с использованием синтаксиса записей).Таким образом, вы выполняете любую работу, которую вам нужно выполнить, как некоторую комбинацию функций, отображающую эти «конструкции данных» в новые конструкции данных, в зависимости от ситуации.

Теперь, если вам абсолютно требуетсякакая-то модель состояния (и, действительно, для ответа на этот вопрос требуется немного опыта, чтобы точно знать , что такое локальное или глобальное состояние ), только тогда вы бы углубились в использование StateМонада, с чем-то вроде:

module StateGame where

import Control.Monad.State

-- Example use of State monad
-- Passes a string of dictionary {a,b,c}
-- Game is to produce a number from the string.
-- By default the game is off, a C toggles the
-- game on and off. A 'a' gives +1 and a b gives -1.
-- E.g 
-- 'ab'    = 0
-- 'ca'    = 1
-- 'cabca' = 0
-- State = game is on or off & current score
--       = (Bool, Int)

type GameValue = Int
type GameState = (Bool, Int)

playGame :: String -> State GameState GameValue
playGame []     = do
    (_, score) <- get
    return score

playGame (x:xs) = do
    (on, score) <- get
    case x of
         'a' | on -> put (on, score + 1)
         'b' | on -> put (on, score - 1)
         'c'      -> put (not on, score)
         _        -> put (on, score)
    playGame xs

startState = (False, 0)

main = print $ evalState (playGame "abcaaacbbcabbab") startState

(бесстыдно снято с этого урока ).Обратите внимание на использование аналогичных «чисто неизменяемых структур данных» в контексте монады состояния, в дополнение к монадическим функциям «put» и «get», которые облегчают доступ к состоянию, содержащемуся в монаде состояния..

В конце концов, я бы посоветовал вам спросить себя: что вы действительно хотите достичь с помощью этой модели (ОО) класса?Хаскель не является вашим типичным ОО-языком, и попытка отобразить концепции более 1-к-1 только разочарует вас в краткосрочной (и, возможно, долгосрочной) перспективе.Это должна быть стандартная мантра, но я очень рекомендую учиться из книги Real World Haskell , где авторы могут углубиться в гораздо более детальную «мотивацию» для выбора любого одного инструмента или абстракции над другим,Если бы вы были непреклонны, вы могли бы смоделировать традиционные ОО-конструкции в Haskell, но я бы не советовал делать это - если у вас нет действительно веских причин для этого.

3 голосов
/ 01 июня 2011

Требуется немного перестановок, чтобы преобразовать императивный код в чисто функциональный контекст.

Сеттер изменяет объект.Нам не разрешено делать это непосредственно в Хаскеле из-за лени и чистоты.

Возможно, если мы переведем Государственную монаду на другой язык, это будет более очевидным.Ваш код написан на C ++, но, поскольку я, по крайней мере, хочу сборку мусора, я буду использовать здесь Java.

Поскольку Java никогда не удосужилась определить анонимные функции, сначала мы определим интерфейс для чистых функций.

public interface Function<A,B> {
    B apply(A a);
}

Тогда мы можем создать чистый неизменный тип пары.

public final class Pair<A,B> {
    private final A a;
    private final B b;
    public Pair(A a, B b) {
        this.a = a;
        this.b = b;
    }
    public A getFst() { return a; }
    public B getSnd() { return b; }
    public static <A,B> Pair<A,B> make(A a, B b) {
        return new Pair<A,B>(a, b);
    }
    public String toString() {
        return "(" + a + ", " + b + ")";
    }
}

Имея в руках, мы можем фактически определить монаду State:

public abstract class State<S,A> implements Function<S, Pair<A, S> > {

    // pure takes a value a and yields a state action, that takes a state s, leaves it untouched, and returns your a paired with it. 
    public static <S,A> State<S,A> pure(final A a) {
        return new State<S,A>() {
            public Pair<A,S> apply(S s) {
                return new Pair<A,S>(a, s);
            }
        };
    }
    // we can also read the state as a state action.
    public static <S> State<S,S> get() {
        return new State<S,S>() {
            public Pair<S,S> apply(S, s) {
                return new Pair<S,S>(s, s);
            }
        } 
    }

    // we can compose state actions
    public <B> State<S,B> bind(final Function<A, State<S,B>> f) {
        return new State<S,B>() {
            public Pair<B,S> apply(S s0) {
                Pair<A,S> p = State.this.apply(s0);
                return f.apply(p.getFst()).apply(p.getSnd());
            }
        };
    }

    // we can also read the state as a state action.
    public static <S> State<S,S> put(final S newS) {
        return new State<S,S>() {
            public Pair<S,S> apply(S, s) {
                return new Pair<S,S>(s, newS);
            }
        } 
    }
}

Теперь существует понятие геттера и сеттера внутри государственной монады.Это так называемые линзы.Базовая презентация на Java будет выглядеть так:

public abstract class Lens[A,B] {
    public abstract B get(A a);
    public abstract A set(B b, A a);
    // .. followed by a whole bunch of concrete methods.
}

Идея состоит в том, что линза обеспечивает доступ к получателю, который знает, как извлечь B из A, и к установщику, который знает, как взять Bи некоторый старый A, и заменить часть A, получая новый A. Он не может изменить старый, но может создать новый с одним из замененных полей.

Я выступил с докладом.на них на недавней встрече энтузиастов Скала в Бостоне.Вы можете посмотреть презентацию здесь .

Чтобы вернуться в Haskell, вместо того, чтобы говорить о вещах в императивной обстановке.Мы можем импортировать

import Data.Lens.Lazy

из comonad-transformers или и другие библиотеки линз, упомянутые здесь .Эта ссылка предоставляет законы, которые должны быть соблюдены, чтобы быть действительным объективом.

И тогда вам нужен тип данных:

data A = A { x_ :: Int }

с объективом

x :: Lens A Int
x = lens x_ (\b a -> a { x_ = b })

Теперь вы можете написать код, который выглядит как

postIncrement :: State A Int
postIncrement = do
    old_x <- access x
    x ~= (old_x + 1)
    return old_x

, используя комбинаторы из Data.Lens.Lazy .

Другие библиотеки объективов, упомянутые вышепредоставить аналогичные комбинаторы.

0 голосов
/ 01 июня 2011

Прежде всего, я согласен с Raeez, что это, вероятно, неправильный путь, если вы действительно не знаете, почему! Если вы хотите увеличить какое-то значение на 42 (скажем), почему бы не написать функцию, которая сделает это за вас?

Это довольно сильно отличается от традиционного ОО-мышления, когда у вас есть коробки со значениями в них, и вы вынимаете их, манипулируете ими и вставляете их обратно. Я бы сказал, что пока вы не начнете замечать паттерн "Эй! какое-то значение в качестве аргумента, а в конце возвращаемое значение слегка модифицируется, коррелируется с другим значением, и вся сантехника становится грязной! " вам не нужна монада State. Часть удовольствия (обучения) на Haskell - поиск новых способов обойти государственное ОО мышление!

Тем не менее, если вам абсолютно нужна коробка с x типа Int, вы можете попробовать создать свои собственные варианты get и put, например, так:

import Control.Monad.State

data Point = Point { x :: Int, y :: Int } deriving Show

getX :: State Point Int
getX = get >>= return . x

putX :: Int -> State Point ()
putX newVal = do
    pt <- get
    put (pt { x = newVal })

increaseX :: State Point ()
increaseX = do
    x <- getX
    putX (x + 1)

test :: State Point Int
test = do
    x1 <- getX
    increaseX
    x2 <- getX
    putX 7
    x3 <- getX
    return $ x1 + x2 + x3

Теперь, если вы оцените runState test (Point 2 9) в ghci, вы вернетесь (12,Point {x = 7, y = 9}) (так как 12 = 2 + (2 + 1) + 7 и x в состоянии устанавливается на 7 в конце ). Если вас не волнует возвращаемая точка, вы можете использовать evalState, и вы получите только 12.

Здесь также есть более сложные вещи, такие как абстрагирование Point с классом типов на случай, если у вас есть несколько структур данных, которые имеют что-то вроде x, но, на мой взгляд, лучше оставить еще один вопрос!

...