<![CDATA[Old Lisper]]>http://1ambda.github.io/Ghost 0.7Fri, 15 Apr 2016 00:09:43 GMT60<![CDATA[Redux 와 Webpack 을 사용할 때 알아두면 도움이 될 9 가지]]>

지난 한달 동안 자그마한 웹앱 프로젝트를 Redux 를 이용해서 진행했습니다. 그 과정에서 배운 몇 가지를 적었습니다.

Redux: 1. combineReducers 를 이용해 Reducer 를 잘게 분해하기
Redux: 2. Reducer 에서는 관련있는 Action 만 처리하기
Redux: 3. redux internal 이해하기
Redux: 4. redux-saga 사용하기
Redux: 5. API 호출 실패에 대한 액션을 여러개

]]>
http://1ambda.github.io/tips-redux-and-webpack/1877a1e2-c38b-4f1f-88ec-28d4e3edcd13Thu, 14 Apr 2016 04:36:24 GMT

지난 한달 동안 자그마한 웹앱 프로젝트를 Redux 를 이용해서 진행했습니다. 그 과정에서 배운 몇 가지를 적었습니다.

Redux: 1. combineReducers 를 이용해 Reducer 를 잘게 분해하기
Redux: 2. Reducer 에서는 관련있는 Action 만 처리하기
Redux: 3. redux internal 이해하기
Redux: 4. redux-saga 사용하기
Redux: 5. API 호출 실패에 대한 액션을 여러개 만들지 않기
Webpack: 6. 테스팅 프레임워크로 jest 대신 mocha 사용하기
Webpack: 7. postcss 를 사용할 경우, 테스팅 환경에서 스타일파일 무시하기
Webpack: 8. DefinePlugin 을 이용해 클라이언트 파일에 환경변수 주입하기
Etc: 9. json-server 사용하기

Redux

1. combineReducers 를 이용해 Reducer 를 잘게 분해하기

Root Reducer 가 JobReducer 를 포함하고 있고, JobReducer 는 Job 과 관련된 모든 상태를 다룬다고 할 때 다음처럼 combineReducers 를 이용해서 JobReducer 를 분해하면, 개별 컴포넌트의 상태(State) 는 각각의 서브 리듀서 (이하 핸들러) 에서 다루면 됩니다.

// https://github.com/1ambda/slott/blob/master/src/reducers/JobReducer/index.js

import { combineReducers, } from 'redux'

import * as JobItemState from './JobItemState'  
import * as PaginatorState from './PaginatorState'  
import * as FilterState from './FilterState'  
import * as SorterState from './SorterState'  
...

export const JOB_STATE_PROPERTY = {  
  JOB_ITEMS: 'items',
  PAGINATOR: 'paginator',
  FILTER: 'filterKeyword',
  SORTER: 'sortingStrategy',
  ...
}

export default combineReducers({  
  [JOB_STATE_PROPERTY.CONTAINER_SELECTOR]: ContainerSelectorState.handler,
  [JOB_STATE_PROPERTY.JOB_ITEMS]: JobItemState.handler,
  [JOB_STATE_PROPERTY.PAGINATOR]: PaginatorState.handler,
  [JOB_STATE_PROPERTY.FILTER]: FilterState.handler,
  [JOB_STATE_PROPERTY.SORTER]: SorterState.handler,
  ...
})
// https://github.com/1ambda/slott/blob/master/src/reducers/JobReducer/FilterState.js

import { createAction, handleActions, } from 'redux-actions'

const INITIAL_STATE = '' /** initial state of FilterState */

export const ActionType = {  
  FILTER: 'JOB_FILTER',
  INITIALIZE_FILTER: 'JOB_INITIALIZE_FILTER',
}

export const Action = {  
  filterJob: createAction(ActionType.FILTER),
  initializeFilter: createAction(ActionType.INITIALIZE_FILTER),
}

export const handler = handleActions({  
  [ActionType.FILTER]: (state, { payload, }) =>
    payload.filterKeyword, /** since string is immutable. we don't need to copy old state */

  [ActionType.INITIALIZE_FILTER]: (state, { payload, }) =>
    INITIAL_STATE,
}, INITIAL_STATE)

2. Reducer, Action, ActionType 을 한 파일로 모으기

Redux Github 에 나와있는 예제에서는 ActionTypeAction 을 하나의 파일에 모아놓는데, 프로젝트가 커질수록 부담스럽습니다.

Action, ActionType, Handler 를 한 파일에 모아놓으면 이 핸들러가 어떤 일들을 하는지, 페이로드는 무엇인지 한 눈에 파악할 수 있습니다.

// https://github.com/1ambda/slott/blob/master/src/reducers/JobReducer/PaginatorState.js

import { createAction, handleActions, } from 'redux-actions'

import { PAGINATOR_ITEM_COUNT_PER_PAGE, } from '../../constants/config'

import * as FilterState from './FilterState'  
import * as SorterState from './SorterState'

const INITIAL_PAGINATOR_STATE = {  
  currentPageOffset: 0,
  currentItemOffset: 0,
  itemCountPerPage: PAGINATOR_ITEM_COUNT_PER_PAGE,
}

export const ActionType = {  
  CHANGE_PAGE_OFFSET: 'JOB_CHANGE_PAGE_OFFSET',
}

export const Action = {  
  changePageOffset: createAction(ActionType.CHANGE_PAGE_OFFSET),
}

export const handler = handleActions({  
  [ActionType.CHANGE_PAGE_OFFSET]: (state, { payload, }) => {
    const { newPageOffset, } = payload
    const currentItemOffset = newPageOffset * state.itemCountPerPage
    return Object.assign({}, state, {currentPageOffset: newPageOffset, currentItemOffset,})
  },

  /** reset paginator if filter or sorter action is occurred */
  [SorterState.ActionType.SORT]: (state) => INITIAL_PAGINATOR_STATE,
  [FilterState.ActionType.FILTER]: (state) => INITIAL_PAGINATOR_STATE,
}, INITIAL_PAGINATOR_STATE)

Paginator 가 어떤 액션을 처리하고, 페이로드는 무엇인지 하나의 파일에서 확인할 수 있습니다.

3. redux internal 이해하기

redux 의 놀라운 점중 하나는 소스코드가 길지 않다는 점입니다. 따라서 내부 구조를 이해하기도 어렵지 않은데요,

Redux Middleware: Behind the Scenes 를 참고하면, enhancer 가 어떻게 조합되고, store 가 어떻게 생성되는지 쉽게 알 수 있습니다.

4. redux-saga 사용하기

redux-saga 를 이용하면 Promise 가 들어가는 비동기 로직을 ES7 async 를 이용하는것처럼 작성할 수 있습니다. 추가적으로 사이드이펙트 (e.g API call) 의 선언과 실행 시점을 분리해 테스트를 쉽게 할 수 있도록 도와줍니다.

예를 들어, 초기화 시점에 서버로부터 전체 Job 을 가져오는 로직을 redux-saga 를 이용해 다음처럼 작성할 수 있습니다.

// https://github.com/1ambda/slott/blob/master/src/middlewares/sagas.js#L12

import { fork, call, put, } from 'redux-saga/effects'

import * as SnackbarState from '../reducers/JobReducer/ClosableSnackbarState'  
import * as Handler from './handler'

export function* initialize() {  
  try {
    yield call(Handler.callFetchContainerJobs)
  } catch (error) {
    yield put(
      SnackbarState.Action.openErrorSnackbar(
        { message: 'Failed to fetch jobs', error, }
      )
    )
  }
}

위 코드는 서버로부터 모든 Job 을 가져오고, 그 과정에서 예외가 발생하면 Snackbar 에 예외메세지를 출력하는 Action 을 Reducer 로 보내는 코드입니다. (여기서 Handler.callFetchContainerJobsPromise 를 돌려준다고 보고)

이 때 다음처럼 테스트를 작성할 수 있습니다.

// https://github.com/1ambda/slott/blob/master/src/middlewares/__tests__/sagas.spec.js#L87

  describe('initialize', () => {
    it('should callFetchContainerJobs', () => {
      const gen = Sagas.initialize()
      expect(gen.next().value).to.deep.equal(
        call(Handler.callFetchContainerJobs)
      )

      expect(gen.next().done).to.deep.equal(true)
    })

    it(`should callFetchJobs
        - if exception is occurred,
          put(openErrorSnackbar with { message, error }`, () => {
      const gen = Sagas.initialize()

      expect(gen.next().value).to.deep.equal(
        call(Handler.callFetchContainerJobs)
      )

      const error = new Error('error')
      expect(gen.throw(error).value).to.deep.equal(
        put(ClosableSnackBarState.Action.openErrorSnackbar({ message: 'Failed to fetch jobs', error, }))
      )
    })
  })

위 테스트 코드에서 알 수 있듯이, redux-saga/effectscall 을 호출하는 시점에서 AJAX 이 실행되지 않습니다. 실제로는 call 은 AJAX 실행할것임을 선언 만 합니다. AJAX 은 call 로 부터 생성된 redux 액션이 redux-saga 미들웨어에서 처리되는 순간에 실행 됩니다. call 의 리턴값은, 어떤 redux 액션이 실행될 것인지 알려주는 자바스크립트 객체입니다. 위에서는 이 리턴값을 이용해 테스트를 작성한 것입니다.

// https://github.com/yelouafi/redux-saga/blob/master/docs/basics/DeclarativeEffects.md


{
  CALL: {
    fn: Handler.callFetchContainerJobs,
    args: []  
  }
}

5. API 호출 실패에 대한 액션을 여러개 만들지 않기

redux 나 redux-saga 예제 를 보면, API 실패에 대한 액션을 여러 종류로 만드는 것을 알 수 있습니다.

그러나 일반적으로 예외는 단일화된 방식으로 (e.g 에러 다이어로그, 팝업, 페이지 등) 처리되기 때문에 에러를 다룰 UI 컴포넌트에 대한 1개의 액션만 만드는 것이 더 바람직 합니다. 예를 들어 Snackbar 에서 예외 메세지를 보여준다고 할 때 다음처럼 액션 핸들러를 작성할 수 있습니다.

// https://github.com/1ambda/slott/blob/e2fc9c1260a5c8202ad747c31f5907ff29ab9a94/src/reducers/JobReducer/ClosableSnackbarState.js#L27

export const handler = handleActions({  
  /** snackbar related */
  [ActionType.CLOSE_SNACKBAR]: (state) =>
    Object.assign({}, state, { snackbarMode: CLOSABLE_SNACKBAR_MODE.CLOSE, }),

  [ActionType.OPEN_ERROR_SNACKBAR]: (state, { payload, }) =>
    Object.assign({}, state, {
      snackbarMode: CLOSABLE_SNACKBAR_MODE.OPEN,
      message: `[ERROR] ${payload.message} (${payload.error.message})`,
    }),

  [ActionType.OPEN_INFO_SNACKBAR]: (state, { payload, }) =>
    Object.assign({}, state, {
      snackbarMode: CLOSABLE_SNACKBAR_MODE.OPEN,
      message: `[INFO] ${payload.message}`,
    }),

}, INITIAL_SNACKBAR_STATE)

만약 여러 종류의 API 실패에 대한 액션을 처리하도록 작성했다면, 이런 코드가 되었을 거고 API_FAILED 액션 타입이 삭제되고 추가될 때 마다 수정해야 하므로 변경에 취약했을 것입니다.

const FAILED_API_ACTION_TYPES = [  
  ActionType.LOAD_ALL_JOBS_FAILED,
  ActionType.CREATE_JOB_FAILED,
  ActionType.REMOVE_JOB_FAILED,
  ...
]

const FailureHandlers = FAILED_API_ACTION_TYPES.map(actionType => {  
  return { [actionType]: (state, { payload, }) =>
    Object.assign({}, state, {
      snackbarMode: CLOSABLE_SNACKBAR_MODE.OPEN,
      message: `[ERROR] ${payload.message} (${payload.error.message})`,
    })
  }
})

export const handler = handleActions({  
  /** snackbar related */
  [ActionType.CLOSE_SNACKBAR]: (state) =>
    Object.assign({}, state, { snackbarMode: CLOSABLE_SNACKBAR_MODE.CLOSE, }),

  ...FailureHandlers,

}, INITIAL_SNACKBAR_STATE)

Webpack

6. 테스팅 프레임워크로 jest 대신 mocha 사용하기

jest 는 Facebook 에서 만든 테스팅 프레임워크입니다. 모든 import 는 기본적으로 mocking 됩니다. 따라서 테스트할 .js 파일에서 사용되는 모든 라이브러리도 mocking 됩니다. 이런식으로 테스트 대상만 unmocking 해서 사용할 수 있습니다.

// https://github.com/facebook/jest

jest.unmock('../sum'); // unmock to use the actual implementation of sum

describe('sum', () => {  
  it('adds 1 + 2 to equal 3', () => {
    const sum = require('../sum');
    expect(sum(1, 2)).toBe(3);
  });
});

jest 사용시 주의 할 사항이 두 가지 있습니다.

jest 0.9.0 기준으로 아직 모든 라이브러리가 mocking 되진 않습니다. (e.g redux-saga)
babel 을 사용할 경우 babel-jest 로 테스트 실행이 가능하지만 여기에 postcss 까지 같이 쓸 경우, import (‘*.css) 구문 때문에 테스팅이 불가능합니다. 커스텀 jest 로더를 등록하면, babel-runtime 로딩이 제대로 안되며 webpack-babel-jest 란것도 있으나 제대로 동작하지 않습니다. (관련이슈 jest issue: 334 - How to test with Jest when I'm using webpack)

7. postcss 를 사용할 경우, 테스팅 환경에서 스타일파일 무시하기

postcss 를 이용하면 autoprefixer 등의 각종 플러그인을 사용 가능합니다. 특히 postcss-loader 를 이용하면 지엽적인 css 클래스 생성과 적용이 가능하므로 모듈, 컴포넌트 단위로 관리되는 React 와 같이 쓰기 좋습니다.

그런데, 테스팅 환경에서는 webpack 이 돌지 않으므로 css 파일 임포트가 불가능 하고, 테스트 실행이 안됩니다. 이 경우 ignore-styles 를 이용하거나 mocha 설정을 이용해 css 파일 임포트 문장을 무시할 수 있습니다.

// https://github.com/1ambda/slott/blob/f1e94a9e693c30f466d92a4d3c988b75d5db4118/package.json#L28

"test": "cross-env NODE_ENV=test mocha --reporter progress --compilers js:babel-core/register --recursive \"./src/**/*.spec.js\" --require ignore-styles"

아니면 react-slingshot 처럼 셋업 파일을 분리해서 mocha 설정으로 이용할 수 있습니다.

// https://github.com/coryhouse/react-slingshot/blob/16ec28c9029bf7e2b65b26c22a1c2daadab427a2/tools/testSetup.js

process.env.NODE_ENV = 'production';

// Disable webpack-specific features for tests since
// Mocha doesn't know what to do with them.
require.extensions['.css'] = function () {  
  return null;
};
require.extensions['.png'] = function () {  
  return null;
};
require.extensions['.jpg'] = function () {  
  return null;
};

// Register babel so that it will transpile ES6 to ES5
// before our tests run.
require('babel-register')();  

이후 package.son 에서 "test": "mocha tools/testSetup.js src/**/*.spec.js --reporter 처럼 사용할 수 있습니다.

8. DefinePlugin 을 이용해 클라이언트 파일에 환경변수 주입하기

Webpack: DefinePlugin 을 이용하면 Webpack 실행 시점에 존재하는 변수를 클라이언트에 주입할 수 있습니다. (e.g 환경변수, 별도 파일로 존재하는 설정값 등) 예를 들어

// https://github.com/1ambda/slott/blob/f1e94a9e693c30f466d92a4d3c988b75d5db4118/tools/config.js

import { ENV_DEV, ENV_PROD, ENV_TEST, } from './env'  
import * as DEV_CONFIG from '../config/development.config'  
import * as PROD_CONFIG from '../config/production.config'

const env = process.env.NODE_ENV

export const CONFIG = (env === ENV_DEV) ? DEV_CONFIG : PROD_CONFIG

export const GLOBAL_VARIABLES = { /** used by Webpack.DefinePlugin */  
  'process.env.ENV_DEV': JSON.stringify(ENV_DEV),
  'process.env.ENV_PROD': JSON.stringify(ENV_PROD),
  'process.env.NODE_ENV': JSON.stringify(env),

  /** variables defined in `CONFIG` file ares already stringified */
  'process.env.CONTAINERS': CONFIG.CONTAINERS,
  'process.env.TITLE': CONFIG.TITLE,
  'process.env.PAGINATOR_ITEM_COUNT': CONFIG.PAGINATOR_ITEM_COUNT,
}

좌측이 클라이언트에서 사용할 변수, 우측이 주입할 변수입니다. 이렇게 만든 후 Webpack 설정에서 다음처럼 사용할 수 있습니다.

// https://github.com/1ambda/slott/blob/f1e94a9e693c30f466d92a4d3c988b75d5db4118/webpack.config.js

const getPlugins = function (env) {  
  const plugins = [
    new webpack.optimize.OccurenceOrderPlugin(),
    new webpack.DefinePlugin(GLOBAL_VARIABLES),
    ...
  ]

  /* eslint-disable no-console */
  console.log('Injecting Global Variable'.green)
  console.log(GLOBAL_VARIABLES)
  /* eslint-enable no-console */

...

이 때 몇 가지 주의할 사항이 있습니다.

If the value is a string it will be used as a code fragment. If the value isn’t a string, it will be stringified (including functions). If the value is an object all keys are defined the same way. If you prefix typeof to the key, it’s only defined for typeof calls.

따라서 우측 값이 문자열일 경우, 코드값으로 사용되므로 undefined 로 주입되거나, Webpack 실행시 예외가 발생할 경우는 확인해 보아야 합니다. 실제로 문자열을 주입하고 싶다면 한번 더 문자열로 감싸야 하구요. 이 부분은 문서에도 나와 있습니다.

// https://webpack.github.io/docs/list-of-plugins.html#defineplugin

new webpack.DefinePlugin({  
    VERSION: JSON.stringify("5fa3b9"),
    BROWSER_SUPPORTS_HTML5: true,
    TWO: "1+1",
    "typeof window": JSON.stringify("object")
})

Etc

9. json-server 사용하기

웹 클라이언트 개발 과정에서, API 연동을 하다보면 두 가지 문제점에 마주칩니다.

  • 아직 백엔드가 개발되지 않았는데 연동이 필요할 경우: 테스팅은 mock 등을 어찌어찌 해서 짤 수 있으나, UI 시뮬레이션은 최소한 로컬호스트 개발용 서버라도 갖추어야 하므로 어려움
  • RESTful API 구현: HTTP Status, Methods, URI 등에 대한 학습과 고민이 필요

로컬에서 미리 정의된 리소스를 읽어 표준화된 REST API 서버를 제공하는 json-server 를 이용하면 이 두 가지 문제를 해결할 수 있습니다.

예를 들어 Job 을 /api/jobs 에서 돌려준다고 하면 리소스 파일을 다음처럼 작성할 수 있습니다.

{
  "jobs": [
    {
      "id": "akka-cluster-A-1",
      "tags": [
        "cluster"
      ],
      "active": true,
      "enabled": true,
      "kafka": {
        "topic": "akka-A",
        "consumer-group": "cluster-consumers"
      },
      "hdfs": "/data/akka/cluster-A"
    }
  , ...
  ]
}

추가적으로 라우팅 세팅을 위해 routes.json 파일을 다음처럼 작성하면 됩니다.

{
  "/api/": "/",

json-server 를 사용할 때 두 가지 주의해야 할 점이 있습니다.

1. id 값은 immutable 이고, 모든 리소스는 id 값을 가지고 있어야 합니다. (키 값은 --id 옵션으로 변경 가능함)

따라서 각 Job 의 실행 상태와 설정값을 별개의 리소스가 아니라 (별개의 리소스라면 jobId 를 주어 join 을 해야함) /api/jobs/:id/state, /api/jobs/:id/config 처럼 nested 된 형태로 돌려주고 싶을 때는 routes.json 의 라우팅 트릭을 이용할 수 있습니다.

// https://github.com/1ambda/slott/blob/f1e94a9e693c30f466d92a4d3c988b75d5db4118/resource/routes.json

{
  "/api/": "/",
  "/:resource/:id/state": "/:resource/:id",
  "/:resource/:id/config": "/:resource/:id"
}

이 때, 이 리소스는 별개의 리소스가 아니라 URI 만 매핑된 것이므로 config, state 등에 대한 변경은 HTTP PATCH 메소드로 변경해야 합니다.

2. 모든 리소스 변경은 즉시 파일에 변경됩니다.

따라서 매 실행마다 동일한 리소스로 시작하려면, 리소스 파일을 복사 후 실행하는 간단한 스크립트를 작성하면 됩니다.

// https://github.com/1ambda/slott/blob/f1e94a9e693c30f466d92a4d3c988b75d5db4118/tools/remote.js

import fs from 'fs-extra'

/** initialize resource/remote/db.json */

const resourceDir = 'resource'

// 3개의 서버를 별개로 띄우므로 3벌 복사
const remotes = ['remote1', 'remote2', 'remote3',]

remotes.map(remote => {  
  fs.copySync(`${resourceDir}/${remote}/db.origin.json`, `${resourceDir}/${remote}/db.json`)
})

이후 package.json 에 다음의 스크립트를 작성하고, 사용하면 됩니다.

// https://github.com/1ambda/slott/blob/f1e94a9e693c30f466d92a4d3c988b75d5db4118/package.json#L9

...

 "start:mock-server1": "json-server resource/remote1/db.json --routes resource/routes.json --port 3002",
    "start:mock-server2": "json-server resource/remote2/db.json --routes resource/routes.json --port 3003",
    "start:mock-server3": "json-server resource/remote3/db.json --routes resource/routes.json --port 3004",
    "start:mock-server": "npm-run-all --parallel start:mock-server1 start:mock-server2 start:mock-server3",

...

References

]]>
<![CDATA[10분만에 Github Profile 만들기]]>


Github 데이터를 이용해 프로필을 만들려면

  1. Github API 를 이용해 데이터를 긁어옵니다.
  2. 데이터를 보여줄 웹 어플리케이션 (static) 을 만듭니다.
  3. Github Page (gh-pages) 를 이용해 남들에게 보여줍니다.

이 때, (1) 에서 만든 데이터의 포맷을 정형화하면, 이것을 사용하는 (2) 의 웹 어플리케이션을 일종의 viewer 로 생각할 수 있습니다. 포맷이 고정되어 있으므로 데이터를 사용하는

]]>
http://1ambda.github.io/create-github-profile-in-10-minutes/243afde3-5959-44c5-8dd6-35eefb670c2cMon, 29 Feb 2016 14:10:20 GMT


Github 데이터를 이용해 프로필을 만들려면

  1. Github API 를 이용해 데이터를 긁어옵니다.
  2. 데이터를 보여줄 웹 어플리케이션 (static) 을 만듭니다.
  3. Github Page (gh-pages) 를 이용해 남들에게 보여줍니다.

이 때, (1) 에서 만든 데이터의 포맷을 정형화하면, 이것을 사용하는 (2) 의 웹 어플리케이션을 일종의 viewer 로 생각할 수 있습니다. 포맷이 고정되어 있으므로 데이터를 사용하는 viewer쉽게 교체하거나, 자신이 원하는대로 커스터마이징 할 수 있게 됩니다.


Demo

시작 전에 오늘 만들 결과물의 데모를 보겠습니다.

Demo (Chrome, Firefox, Safari, IE11+)

  • Langauge: 즐겨 사용하는 프로그래밍 언어
  • Repository: 레포지토리 정보 (stargazer, fork count 등)
  • Contribution: 오픈소스 커밋 내역
  • Activity: 최근 활동 내역 (Push, PullRequest 등)

등의 정보를 확인할 수 있습니다. 커스터마이징 등은 아래에서 설명하겠습니다.

Prerequisites

이제 Github 프로필을 만들어 보겠습니다. 준비물을 먼저 확인하면,

  1. SSH Key 가 Github 에 등록이 안되어있을 경우 Github: Generating an SSH key 를 참조해서 등록해주세요.
  2. oh-my-github 란 이름의 Github Repository 를 만들어주세요. Github Page 를 이용해 배포시 사용할 저장소입니다. (이름은 반드시 oh-my-github 여야 합니다)
  3. Github Access Token 을 만들어주세요. 50 개 이상의 Github API 호출을 위해선 Access Token 이 꼭 필요합니다. (Write Permission 은 필요 없습니다.)

데이터를 보여주는 정적 웹 어플리케이션인 viewer 와 Github API 를 호출해 데이터를 생성하는 oh-my-github 설치법은 아래서 설명하겠습니다.

Install: Viewer

먼저 default viewer 를 클론 받고, upstream 업데이트를 위해 remote 를 등록합니다.

$ git clone git@github.com:oh-my-github/viewer.git oh-my-github
$ cd oh-my-github

$ git remote add upstream git@github.com:oh-my-github/viewer.git

그리고, 위에서 만든 자신의 레포지토리 url 을 origin 으로 등록합니다. [GITHUB_ID] 대신 자신의 아이디를 사용하면 됩니다.

$ git remote remove origin
$ git remote add origin git@github.com:[GITHUB_ID]/oh-my-github

Install: oh-my-github

먼저 oh-my-github 를 설치하겠습니다. NodeJS 가 없다면, NVM 설치 후 문서에 나와있는 대로 NodeJS 5.0.0 이상 버전을 설치해 주세요. 이 문서에서는 5.0.0 을 사용하겠습니다.

$ nvm use 5.0.0
Now using node v5.0.0

$ nvm ls
   v0.12.9
->  v5.0.0
    v5.4.1

이제 oh-my-github 를 설치합니다. 네트워크 상황에 따라 2분 ~ 4분정도 걸립니다.

$ npm install oh-my-github -g

만약 Linux 를 사용하고 있고, 위 설치 과정에서 LIBXXX 등의 에러를 마주쳤을 경우 Linux Install Guide 를 참조해주세요.

이제 viewer 를 클론 받은 디렉토리로 이동한 뒤 oh-my-github 를 실행합니다. 여기서 [GITHUB_TOKEN] 은 위에서 만든 Github Access Token 값이고, [GITHUB_ID] 는 자신의 Github ID 입니다.

$ oh-my-github

$ omg init [GITHUB_ID] oh-my-github       # (e.g) omg init 1ambda oh-my-github
$ omg generate [GITHUB_TOKEN]             # (e.g) omg generate 394fbad49191aca

omg generate 를 실행하면, 현재 디렉토리에 oh-my-github.json (프로필 데이터) 가 생성됩니다. Github Page 에 배포하기 전에 먼저 로컬에 띄워 볼 수 있습니다.

$ omg preview

이제 gh-pages 브랜치를 만들고 push 를 해야하는데, 아래의 명령어를 이용해 한번에 해결할 수 있습니다.

$ omg publish

만약 omg publish 명령어가 동작하지 않는다면, 직접 Git 커맨드를 사용하면 됩니다.

$ git add --all
$ git commit -m "feat: Update Profile"
$ git checkout -b gh-pages
$ git push origin HEAD

이제 30초 정도 기다리고, 자신의 oh-my-github 레포지토리 Github Page URL 을 확인해 봅니다. 예를 들어 Github ID 가 1ambda 라면, http://1ambda.github.io/oh-my-github 에 프로필이 생성됩니다.


Update

Profile

프로필 데이터 oh-my-github.json 내용은 크게 분류하면 두가지로 나뉩니다.

  • activities: 사용자의 활동 정보로, 이전 정보에 새로운 값이 추가됨(append)
  • repositories, languages 등: 최신 정보로 덮어 씌워짐 (overwrite)

omg generate 를 실행할 때 마다, 새로운 이벤트가 있다면 activities 값이 추가 (append) 됩니다.

더 정확히는, Github API 는 최대 10개월 혹은 최대 300개의 event 만 제공하기 때문에, 이것보다 더 많은 양의 event 를 프로필 데이터에 저장하고자 event id 값으로 중복 제거를 한 뒤 append 방식으로 데이터를 쌓습니다.

따라서 프로필 데이터를 업데이트 하고, Github 에 푸시하려면 다음의 명령어를 실행하면 됩니다.

$ cd oh-my-github         # oh-my-github.json 이 위치한 곳

$ omg generate [GITHUB_TOKEN]
$ omg publish

Viewer

viewerupstream 에서 다음처럼 업데이트 할 수 있습니다.

$ cd oh-my-github         # oh-my-github.json 이 위치한 곳

$ git checkout master
$ git pull upstream master --rebase

$ git checkout gh-pages
$ git rebase master

$ git push origin HEAD


Customizing

만약 default viewer 가 맘에 들지 않는다거나, 새로운 기능 (e.g 그래프) 을 추가하고 싶다면 클론받아 app/src 아래의 코드를 수정할 수 있습니다.

app  
├── LICENSE.md
├── package.json
├── src (웹 애플리케이션 소스)
│   ├── actions
│   ├── components
│   ├── constants
│   ├── containers
│   ├── reducers
│   ├── store
│   ├── theme
│   └── util
└── tools (빌드도구 관련)

app 디렉토리로 이동 후 npm start -s 를 실행하고 코드를 수정한 뒤, npm run build 를 실행하면 루트 디렉토리에 bundle.jsindex.html 이 업데이트 됩니다. 이 두 파일을 자신의 oh-my-github 에 업데이트 하면 됩니다.

추가로, 다른 사람들이 자신이 수정한 viewer 를 찾을 수 있게 NPM 에 등록하고 싶다면 package.json 을 수정 후 app 디렉토리에서 npm publish 명령을 실행하면 됩니다.

아래의 내용을 수정하고, 배포하면 NPM 에서 oh-my-github, viewer 키워드로 검색할 수 있습니다. (NPM: oh-my-github, viewer)

{
  ...

  "name": "oh-my-github-viewer-default",
  "version": "0.0.1",
  "author": "1ambda",
  "description": "",
  "homepage": "https://github.com/oh-my-github/viewer#readme",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/oh-my-github/viewer.git"
  },
  "bugs": {
    "url": "https://github.com/oh-my-github/viewer/issues"
  },

  ...
}

References

]]>
<![CDATA[Easy Scalaz 5, Playing with Monoids]]>

이번 글에서는 모노이드를 가지고 놀면서, 아래 나열된 라이브러리 및 언어적 특성을 살펴보겠습니다.

Monoid

Easy Scalaz 4 - Yoneda and Free Monad: Monoid 부분에서 발췌하면,

어떤

]]>
http://1ambda.github.io/easy-scalaz-5-playing-with-monoids/c86052aa-a361-4372-b3f2-00fc3e58e49bFri, 18 Dec 2015 15:37:58 GMT

이번 글에서는 모노이드를 가지고 놀면서, 아래 나열된 라이브러리 및 언어적 특성을 살펴보겠습니다.

Monoid

Easy Scalaz 4 - Yoneda and Free Monad: Monoid 부분에서 발췌하면,

어떤 집합 S 에 대한 닫힌 연산 *, 집합 내의 어떤 원소 e 가 다음을 만족할 경우 모노이드라 부릅니다.

  • e * a = a = a * e (identity)
  • (a * b) * c = a * (b * c) (associativity)

일반적으로 e 를 항등원이라 부릅니다. Option[A]None 을 항등원으로 사용하고, associativity 를 만족하는 A 의 연산을 사용하면 모노이드입니다. 따라서 A 가 모노이드면 Option[A] 도 모노이드입니다.

Scalaz 에서는 모노이드 연산 * 를, |+| 로 표시합니다. 우리가 알고 있는 primitives 대부분이 모노이드입니다.

> load.ivy("org.scalaz" % "scalaz-core_2.11" % "7.2.0-M5")

> import scalaz._, Scalaz._
import scalaz._, Scalaz._  
> implicitly[Monoid[String]]
res4: Monoid[String] = scalaz.std.StringInstances$stringInstance$@5590d10f  
> implicitly[Monoid[Int]]
res5: Monoid[Int] = scalaz.std.AnyValInstances$$anon$5@4b9f2522  
> implicitly[Monoid[Set[Int]]]
res6: Monoid[Set[Int]] = scalaz.std.SetInstances$$anon$3@5b1965ea

> "1" |+| "2"
res7: String = "12"  
> 1.0 |+| 2.0
Compilation Failed  
Main.scala:1459: value |+| is not a member of Double  
1.0 |+| 2.0  
    ^
> 1 |+| 2
res8: Int = 3

> 1.some |+| 2.some
res11: Option[Int] = Some(3)  
> 1.some |+| none
res12: Option[Int] = Some(1)  
> none[Int] |+| 1.some
res13: Option[Int] = Some(1)  

Map[A, B]AKey 로 잡고, B 의 모노이드 연산과 항등원을 이용하는 모노이드입니다.

> val m1 = Map("a" -> 1, "b" -> 2)
m1: Map[String, Int] = Map("a" -> 1, "b" -> 2)  
> val m2 = Map("a" -> 1, "c" -> 2)
m2: Map[String, Int] = Map("a" -> 1, "c" -> 2)  
> m1 |+| m2
res16: Map[String, Int] = Map("a" -> 2, "c" -> 2, "b" -> 2)  

Boolean Monoid

Boolean 의 경우에는, 두 가지 모노이드가 존재할 수 있습니다.

  • && 를 연산으로 사용하고, true 를 항등원으로 사용하는 경우
  • || 를 연산으로 사용하고, false 를 항등원으로 사용하는 경우

첫 번째를 Conjunction 이라 부르고 두 번째를 Disjunction 이라 부릅니다. 즉, Boolean 은 두 개의 모노이드가 존재할 수 있기 때문에 아래처럼 scalaz|+| 를 바로 이용할 수 없습니다. Disjunction 인지 Conjunction 인지 골라야 하기 때문입니다.

> false |+| false
Compilation Failed  
Main.scala:1468: value |+| is not a member of Boolean  
false |+| false  
      ^

// import 를 하지 않으면, scalaz.Tags.Disjunction 이 아니라 scalaz.Disjunction 을 사용하므로 주의
> import scalaz.Tags._
import scalaz.Tags._  
> import scalaz.syntax.tag._
import scalaz.syntax.tag._  
> Disjunction(false)
res22: Boolean @@ Disjunction = false  
> Conjunction(false)
res23: Boolean @@ Conjunction = false

> implicitly[Monoid[Boolean @@ Disjunction]]
res27: Monoid[Boolean @@ Disjunction] = scalaz.std.AnyValInstances$$anon$7@79a6c868  
> implicitly[Monoid[Boolean @@ Conjunction]]
res28: Monoid[Boolean @@ Conjunction] = scalaz.std.AnyValInstances$$anon$8@6e49df4a

> Disjunction(false) |+| Disjunction(true)
res29: Boolean @@ Disjunction = true  
> Disjunction(true) |+| Disjunction(false)
res30: Boolean @@ Disjunction = true  
> Conjunction(true) |+| Conjunction(true)
res31: Boolean @@ Conjunction = true  
> Conjunction(true) |+| Conjunction(false)
res32: Boolean @@ Conjunction = false

> List(false, false, true, false)
res37: List[Boolean] = List(false, false, true, false)  
> Disjunction.subst(res37).suml
res38: Boolean @@ Disjunction = true  
> Conjunction.subst(res37).suml
res39: Boolean @@ Conjunction = false  

실제로 scalaz.std.AnyVal 을 확인해 보면,

// https://github.com/scalaz/scalaz/blob/series/7.2.x/core/src/main/scala/scalaz/std/AnyVal.scala#L52

object conjunction extends Monoid[Boolean] {  
  def append(f1: Boolean, f2: => Boolean) = f1 && f2
  def zero: Boolean = true
}

object disjunction extends Monoid[Boolean] {  
  def append(f1: Boolean, f2: => Boolean) = f1 || f2
  def zero = false
}

그렇다면 Int 의 경우에도 * 등 다른 모노이드가 있는데 왜 + 연산과 0 항등원만 |+| 에서 사용하는걸까요? 이는 + 가 너무 보편적이기 때문이며, * (곱셈) 등은 위에서 본 Tag 를 이용해 모노이드 연산으로 지정할 수 있습니다.

Tag

// https://github.com/scalaz/scalaz/blob/series/7.2.x/core/src/main/scala/scalaz/Tags.scala

object Tags {

  ...

  /** Type tag to choose a [[scalaz.Monoid]] instance that selects the lesser of two operands, ignoring `zero`. */
  sealed trait Min

  val Min = Tag.of[Min]

  /** Type tag to choose a [[scalaz.Monoid]] instance that selects the greater of two operands, ignoring `zero`. */
  sealed trait Max

  val Max = Tag.of[Max]

  /** Type tag to choose a [[scalaz.Monoid]] instance for a numeric type that performs multiplication,
   *  rather than the default monoid for these types which by convention performs addition. */
  sealed trait Multiplication

  val Multiplication = Tag.of[Multiplication]

  ...
}

Multiplication 을 이용하면,

> Multiplication(2) |+| Multiplication(6)
res3: Int @@ Multiplication = 12

> implicitly[Monoid[Int @@ Multiplication]]
res4: Monoid[Int @@ Multiplication] = scalaz.std.AnyValInstances$$anon$12@5910ca72  

AnyValInstances 를 찾아보면 byteMultiplicationNewType, intMultiplicationNewTypeA @@ Multiplication 을 위한 인스턴스들이 구현되어 있습니다.

// https://github.com/scalaz/scalaz/blob/series/7.2.x/core/src/main/scala/scalaz/std/AnyVal.scala#L253

trait AnyValInstances {

  implicit val shortMultiplicationNewType: Monoid[Short @@ Multiplication] with Enum[Short @@ Multiplication] = new Monoid[Short @@ Multiplication] with Enum[Short @@ Multiplication] {
    ...
  } 

  implicit val intMultiplicationNewType: Monoid[Int @@ Multiplication] with Enum[Int @@ Multiplication] = new Monoid[Int @@ Multiplication] with Enum[Int @@ Multiplication] {
    ...
  }
}

Tag 는 이렇게 생겼습니다.

// https://github.com/scalaz/scalaz/blob/series/7.2.x/core/src/main/scala/scalaz/package.scala#L99

package object scalaz {  
  ...

  private[scalaz] type Tagged[A, T] = {type Tag = T; type Self = A}

  /**
   * Tag a type `T` with `Tag`.
   *
   * The resulting type is used to discriminate between type class instances.
   *
   * @see [[scalaz.Tag]] and [[scalaz.Tags]]
   *
   * Credit to Miles Sabin for the idea.
   */
  type @@[T, Tag] = Tagged[T, Tag]

  ...
}

@@[A, T] 를 생성하기 위해 Tag.apply 를 값을 추출하기 위해 unwrap 을 이용할 수 있습니다.

// https://github.com/scalaz/scalaz/blob/series/7.2.x/core/src/main/scala/scalaz/Tag.scala
object Tag {  
  /** `subst` specialized to `Id`.
    *
    * @todo According to Miles, @specialized doesn't help here. Maybe manually specialize.
    */
  @inline def apply[@specialized A, T](a: A): A @@ T = a.asInstanceOf[A @@ T]

  /** `unsubst` specialized to `Id`. */
  @inline def unwrap[@specialized A, T](a: A @@ T): A = unsubst[A, Id, T](a)

  /** Add a tag `T` to `A`.
    *
    * NB: It is unsafe to `subst` or `unsubst` a tag in an `F` that is
    * sensitive to the `A` type within.  For example, if `F` is a
    * GADT, rather than a normal ADT, it is probably unsafe.  For
    * "normal" types like `List` and function types, it is safe.  More
    * broadly, if it is possible to write a ''legal''
    * [[scalaz.InvariantFunctor]] over the parameter, `subst` of that
    * parameter is safe.
    * 
    * We do not have a
    * <a href="https://ghc.haskell.org/trac/ghc/wiki/Roles">type role</a>
    * system in Scala with which to declare the exact situations under
    * which `subst` is safe.  If we did, we would declare that `subst`
    * is safe if and only if the parameter has "representational" or
    * "phantom" role.
    */
  def subst[A, F[_], T](fa: F[A]): F[A @@ T] = fa.asInstanceOf[F[A @@ T]]

  ...
}

TagValue Class 처럼 활용할 수도 있는데요,

// http://eed3si9n.com/learning-scalaz/Tagged+type.html

sealed trait USD  
sealed trait EUR  
def USD[A](amount: A): A @@ USD = Tag[A, USD](amount)  
def EUR[A](amount: A): A @@ EUR = Tag[A, EUR](amount)

val oneUSD = USD(1)  

태깅된 타입을 이용하면 implicit 를 선택할 수 있습니다. 예를 들어

implicit val anonymousUserWriter = Writer[User @@ Anonymous] { ... }  
implicit val loggedInUserWriter  = Writer[User @@ LoggedIn]  { ... }  

그러나 type B = A @@ T 에서 BA 의 서브타입으로 취급되므로 주의하여 사용해야 합니다. 예를 들어, scalatest===, shouldBe 는 런타임값만 체크하므로 아래는 항상 참입니다.

def convertUSDtoEUR[A](usd: A @@ USD, rate: A)  
                      (implicit M: Monoid[A @@ Multiplication]): A @@ EUR =
  EUR((Multiplication(usd.unwrap) |+| Multiplication(rate)).unwrap)

convertUSDtoEUR(USD(1), 2) === EUR(2) // true  
convertUSDtoEUR(USD(1), 2) === USD(2) // true

convertUSDtoEUR(USD(1), 2) shouldBe EUR(2) // true  
convertUSDtoEUR(USD(1), 2) shouldBe USD(2) // true

2 shouldBe USD(2) // true  
2 shouldBe EUR(2) // true  

따라서 =:= 를 만들어 사용하면 EURUSD 비교시 컴파일 예외를 발생시킬 수 있습니다. (더 정확히는 scalaz=== 또는 org.scalactic.TypeCheckedTripleEquals 를 사용하면 되는데, org.scalactic.TripleEqualSupportsFunSuite 내에서 하이딩 시킬 방법을 찾지 못해서 아래처럼 구현했습니다.)

// impilcit class 로 만들고 import 해서 사용해도 상관없음
trait TestImplicits {  
  final case class StrictEqualOps[A](val a: A) {
    def =:=(aa: A) = assert(a == aa)
    def =/=(aa: A) = assert(!(a == aa))
  }

  implicit def toStrictEqualOps[A](a: A) = StrictEqualOps(a)
}

// spec
convertUSDtoEUR(USD(1), 2) =:= EUR(2)  
convertUSDtoEUR(USD(1), 2) =:= EUR(3) // will fail  
convertUSDtoEUR(USD(1), 2) =:= USD(3) // compile error  

Tag 을 이용하면 같은 primitive type 이어도 별도의 wrapper 를 만들지 않으면서 다른 타입으로 만들 수 있습니다. 예를 들어 JobAgent 가 수행한다고 하면, 다음과 같이 간단한 모델을 만들어 볼 수 있는데

// ref - http://www.slideshare.net/IainHull/improving-correctness-with-types

case class Agent(id: String, /* agent id */  
                 status: String, /* agent status */
                 jobType: String)

case class Job(id: String, /* job id */  
               maybeAgentId: Option[String], /* agent id */
               status: String, /* job status */
               jobType: String)

여기서 Sum 을 먼저 추출하면, (Algebraic Data Type 관련해서는 Sum and Product 참조)

sealed abstract class AgentStatus(val value: String)  
case object Waiting    extends AgentStatus("WAITING")  
case object Processing extends AgentStatus("PROCESSING")

sealed abstract class JobStatus(val value: String)  
case object Created   extends JobStatus("CREATED")  
case object Allocated extends JobStatus("ALLOCATED")  
case object Completed extends JobStatus("COMPLETED")

sealed abstract class JobType(val value: String)  
case object Small extends JobType("SMALL")  
case object Large extends JobType("LARGE")  
case object Batch extends JobType("BATCH")

case class Agent(id: String, /* agent id */  
                 status: AgentStatus,
                 jobType: JobType)

case class Job(id: String, /* job id */  
               maybeAgentId: Option[String], /* agent id */
               status: JobStatus,
               jobType: JobType)

여기서 오류의 소지가 다분한 id 에 태깅을 하면 다음과 같습니다.

import scalaz._

case class Agent(id: String @@ Agent,  
                 status: AgentStatus,
                 jobType: JobType)

case class Job(id: String @@ Job,  
               maybeAgentId: Option[String @@ Agent],
               status: JobStatus,
               jobType: JobType)

Agent(Tag[String, Agent]("03"), Waiting, Small)  
Job(Tag[String, Job]("03"), None, Created, Small)  

조금 더 개선할 여지는, maybeAgentIdOption 을 이용하는 대신, agent 에 할당된 job 과 아닌 job 을 서브타입으로 분리하면, Job 을 다루는 함수에서 Option 처리를 피할 수 있습니다.

물론 이는 디자인적 결정입니다. Option 을 허용하되 수퍼클래스를 인자로 받을것인가, 아니면 허용하지 않을것인가의 문제죠. 개인적으로는 프로그래밍 과정에서 타입을 점점 좁혀가면 오류의 여지를 줄일 수 있기 때문에 후자를 선호합니다. 그렇지 않으면 강력한 타입시스템을 갖춘 언어를 굳이 사용할 필요가 없겠지요.

타입을 이용한 오류방지 방법 관련해서 Improving Correctness with Types 를 읽어보시길 권합니다.

Monoid Example: Filter

간단한 Monoid 예제를 하나 만들어 보겠습니다. User 클래스가 있고, 필터링을 하고 싶을 때

// http://www.slideshare.net/oxbow_lakes/practical-scalaz

case class User(name: String, city: String)  
type Filter[A] = A => Boolean // Function1, same as Reader[A, Boolean]

val london: Filter[User] = _.city endsWith(".LONDON")  
val ny: Filter[User]     = _.city endsWith(".NY")

val inLondon = users filter london  
val inNY = users filter ny  

이 때 만약 Filter[A]OR (||) 연산에 대한 모노이드라면, 이렇게 쓸 수 있지 않을까요?

users filter (london |+| ny)  

그런데 Filter[A] 는 모노이드가 아니기 때문에 그럴 수 없습니다. 우린 모노이드를 배운 사람들이니까 지성인 한 번 만들어 보겠습니다.

implicit def booleanMonoid[A] = new Monoid[Filter[A]] = {  
  override def zero: Filter[A] = 
    false
  override def append(f1: Filter[A], f2: => Filter[A]): Filter[A] = 
    a => f1(a) || f2(a)
}

disjunction 이죠? Scalaz 어딘가에 구현되어 있을것 같습니다.

impilcit def booleanMonoid[A] =  
  function1Monoid[A, Boolean](booleanInstance.disjunction)

function1Monoid[A, R] 은 결과값 R 에 대한 모노이드 Monoid[R] 를 필요로 하고 여기에 위에서 봤던 Monoid[Boolean]booleanInstance.disjunction 을 넣으면, 우리가 원했던 Monoid[Filter[A] 가 완성됩니다.

implicit def function1Monoid[A, R](implicit R0: Monoid[R]): Monoid[A => R] = new Function1Monoid[A, R] {  
  implicit def R = R0
}

private trait Function1Monoid[A, R] extends Monoid[A => R] with Function1Semigroup[A, R] {  
  implicit def R: Monoid[R]
  def zero = a => R.zero
}

object disjunction extends Monoid[Boolean] {  
    def append(f1: Boolean, f2: => Boolean) = f1 || f2
    def zero = false
}

그러면 이제 요구사항을 좀 더 까다롭게 해서, 런던에 사는 켈리 또는 뉴욕에 사는 켈리 만 뽑아내려면 어떻게 해야할까요?

// if we have `|*|` representing `Conjunction`

val kelly: Filter[User] = _.name.endsWith("Kelly")  
val myFriendKelly = (london |*| kelly) |+| (ny |*| kelly)  
users filter myFriendKelly  

그런데, scalaz 에서 할당한 모노이드 연산자는 |+| 하나뿐입니다. 따라서 Implicit Class 를 추가하면

implicit class FilterOps[A](fa: Function1[A, Boolean]) {  
  def |*|(other: Function1[A, Boolean]): Function1[A, Boolean] =
    function1Monoid[A, Boolean](booleanInstance.conjunction).append(fa, other)
}

val users = List(  
  User("Kelly", ".LONDON"),
  User("John", ".NY"),
  User("Cark", ".SEOUL"),
  User("Kelly", ".NY"),
  User("Kelly", ".SEOUL")
)

val ks1 = users filter ((london |*| isKelly) |+| (ny |*| isKelly))  
val ks1.size shouldBe 2

// 더 짧게 줄이면, 
val ks2 = users filter ((london |+| ny) |*| isKelly)  

scalaz.Monoid|+| 만을 지원하는 반면, 대수타입에 특화된 SpireBoolean 에 대해 *, + 두 가지 연산을 모두 지원합니다.

import spire.algebra.Rig

implicit def filterRig[A] = new Rig[Filter[A]] {  
  def plus(x: Filter[A], y: Filter[A]): Filter[A] = v => x(v) || y(v)
  def one: Filter[A] = Function.const(true)
  def times(x: Filter[A], y: Filter[A]): Filter[A] = v => x(v) && y(v)
  def zero: Filter[A] = Function.const(false)
} 

import spire.syntax.rig._

users filter ((london + ny) * kelly)  

Monoid with BooleanW, OptionW and Endo

BooleanOption 은, 연산에 if-else, getOrElse 처럼 다른 경우 를 내포하기 때문에, Monoid.zero 와 엮으면 쏠쏠하게 써먹을 수 있습니다.

> load.ivy("org.scalaz" % "scalaz-core_2.11" % "7.2.0-M5")

> import scalaz._, Scalaz._
import scalaz._, Scalaz._

> ~ 1.some      // Some(1).getOrElse(Monoid[Int].zero)
res5: Int = 1  
> ~ none[Int]   // None.getOrElse(Monoid[Int].zero)
res6: Int = 0  
> none[Int] | 3 // None.getOrElse(3)
res7: Int = 3  

Boolean 연산도 살펴보면,

(true  ? 1 | 2) shouldBe 1
(false ? 1 | 2) shouldBe 2
(true  ?? 1) shouldBe 1
(false ?? 1) shouldBe 0 /* raise into zero */
(true  !? 1) shouldBe 0 /* reversed `??` */
(false !? 1) shouldBe 1

?? 는 조건이 참일경우, A 를 아닐 경우 Monoid[A].zero 를 돌려줍니다.

final class BooleanOps(self: Boolean) {  
  ...
  final def ??[A](a: => A)(implicit z: Monoid[A]): A = b.valueOrZero(self)(a)
  final def !?[A](a: => A)(implicit z: Monoid[A]): A = b.zeroOrValue(self)(a)
  ...
}

trait BooleanFunctions {  
  ...
  final def valueOrZero[A](cond: Boolean)(value: => A)(implicit z: Monoid[A]): A = 
    if (cond) value else z.zero
  final def zeroOrValue[A](cond: Boolean)(value: => A)(implicit z: Monoid[A]): A = 
    if (!cond) value else z.zero
  ...
}

Practical Scalaz 에서는 Endo 와 엮어 다음처럼 사용하는걸 보여줍니다. (new Filter 부분을 추출하는것이 더 나은것 같습니다만, 그냥 이렇게도 사용할 수 있다 정도로 알고만 계시면 될 것 같습니다.)

// http://www.slideshare.net/oxbow_lakes/practical-scalaz

<instruments filter="incl">  
  <symbol value="VOD.L" />
  <symbol value="MSFT.O" /> 
</instruments>  
// before
for {  
  e <- xml \ "instrument"
  f <- e.attribute("filter")
} yield
  (if f == "incl") new Filter(instr(e)) else new Filter(instr(e)).neg)

// after
val reverseFilter = Endo[Filter](_.neg)

for {  
  e <- xml \ "instrument"
  f <- e.attribute("filter")
} yield
  (f == "incl") !? reverseFilter apply new Filter(instr(e))

참고로 EndoFunction1[A, A] 입니다. 따라서 Monoid[Endo[A]]identity function 입니다.

final case class Endo[A](run: A => A) {  
  final def apply(a: A): A = run(a)

  /** Do `other`, than call myself with its result. */
  final def compose(other: Endo[A]): Endo[A] = Endo.endo(run compose other.run)

  /** Call `other` with my result. */
  final def andThen(other: Endo[A]): Endo[A] = other compose this
}

trait EndoFunctions {  
  /** Alias for `Endo.apply`. */
  final def endo[A](f: A => A): Endo[A] = Endo(f)

  /** Alias for `Monoid[Endo[A]].zero`. */
  final def idEndo[A]: Endo[A] = endo[A](a => a)

  ...
}

Example: Currency

이제까지 배워왔던 바를 적용해서, 통화를 나타내는 Currency 모델을 만들어 보겠습니다. 위에선 Tag 를 이용했었으니, 이번엔 Value Class 로 만들어 보겠습니다.

object Currency {  
  sealed trait Currency extends Any
  final case class EUR[A](amount: A) extends AnyVal with Currency
  final case class USD[A](amount: A) extends AnyVal with Currency
}

// spec
USD(1) =:= USD(1)  
USD(3) =:= EUR(2) // compile error  

이제 1.USD 등 의 문법을 위해 implicit class 를 추가하면,

Object Currency {  
  ...

  implicit class CurrencyOps[A](amount: A) {
    def EUR = Currency3.EUR(amount)
    def USD = Currency3.USD(amount)
  }
}

// spec
10.USD =:= 10.USD  

이제 같은 통화간 덧셈을 위해, Monoid[USD[A]] 등을 추가할 수 있습니다. |+| 는 기존의 Monoid[A] 를 이용하면 됩니다.

object Currency {  
  import scalaz._, Scalaz._

  ...
  implicit def usdMonoid[A](implicit M: Monoid[A]) = new Monoid[USD[A]] {
    override def zero: USD[A] =
      USD(M.zero)

    override def append(u1: USD[A], u2: => USD[A]): USD[A] =
      USD(M.append(u1.amount, u2.amount))
  }
}

// spec
(10.USD |+| 10.USD) =:= 20.USD

이제 EUR 를 위한 모노이드를 만들어 보겠습니다. 재미삼아 context bound 를 이용해 보면,

object Currency {  
  ...

  implicit def eurMonoid[A : Monoid] = new Monoid[EUR[A]] {
    override def zero: EUR[A] =
      EUR(implicitly[Monoid[A]].zero)

    override def append(e1: EUR[A], e2: => EUR[A]): EUR[A] =
      EUR(implicitly[Monoid[A]].append(e1.amount, e2.amount))
  }
}

통화가 추가될때 마다 매번 반복적으로 모노이드를 추가해야된다는 것이 귀찮으므로, Currency 용 모노이드를 만들겠습니다. Shapeless 를 이용하면, (ShapelessGeneric, Aux 는 아래에서 설명하겠습니다)

object Currency {  
  import scalaz._, Scalaz._
  import shapeless._

  ...
  implicit def currencyMonoid[A : Monoid, C[_] <: Currency]
  (implicit G: Generic.Aux[C[A], A :: HNil]) = new Monoid[C[A]] {
    override def zero: C[A] =
      G.from(implicitly[Monoid[A]].zero :: HNil)

    override def append(c1: C[A], c2: => C[A]): C[A] = {
      val a1: A = G.to(c1).head
      val a2: A = G.to(c2).head

      G.from(implicitly[Monoid[A]].append(a1, a2) :: HNil)
    }
  }
}

이제 통화간 변환을 위한 함수를 추가해보도록 하겠습니다. 이런 문법은 어떨까요?

12.USD to EUR  

그런데, 현재 우리가 가진 디자인에서 EURcase class 이므로 EUR 생성없이 타입만 지정하려면 이정도 문법으로 타협할 수 있겠네요.

24.USD to[EUR]  

Currency 에서 to 구현을 하려면, to[C[_] <: Currency[_]] 정도로 하위 클래스는 퉁친다 해도, 하위 클래스 인스턴스 생성시에 A 가 필요하므로 CurrencyCurrency[A] 로 변경해야 합니다.

object Currency {  
  sealed trait Currency[A] extends Any {
    def amount: A
  }

  final case class EUR[A](amount: A) extends AnyVal with Currency[A]
  final case class USD[A](amount: A) extends AnyVal with Currency[A]

  implicit class CurrencyOps[A](amount: A) {
    def EUR = Currency3.EUR(amount)
    def USD = Currency3.USD(amount)
  }

  implicit def currencyMonoid[A : Monoid, C[A] <: Currency[A]]
  (implicit G: Generic.Aux[C[A], A :: HNil]) = new Monoid[C[A]] {
    override def zero: C[A] =
      G.from(implicitly[Monoid[A]].zero :: HNil)

    override def append(c1: C[A], c2: => C[A]): C[A] = {
      val a1: A = G.to(c1).head
      val a2: A = G.to(c2).head

      G.from(implicitly[Monoid[A]].append(a1, a2) :: HNil)
    }
  }
}

이제 Currencyto 를 추가하면,

object Currency {  
  ...

  sealed trait Currency[A] extends Any {
    def amount: A
    def to[C[A] <: Currency[A]](implicit G: Generic.Aux[C[A], A :: HNil]): C[A] =
      G.from(amount :: HNil)
  }

  ...
}

// spec
(10.USD.to[EUR]) =:= 10.EUR

toimplicit 로 통화간 환율을 담고있는 R: Rate 등을 추가하고 Rate 내에서 Monoid[A @@ Multiplcation 을 이용하면 컴파일타임에

  • USD -> EUR 변환이 정의되어 있는지 (Shapeless Heterogenous Maps)
  • A 에 대한 곱셈 연산 Monoid[A @@ Multiplication] 이 정의 되어있는지를 검사할 수 있습니다.

구현은 숙제로.. 제가 귀찮아서가 절대 아닙니다

디자인적인 결정이겠으나, USD, EUR 등을 object 로 만들고 case class Money[A](amount: A, currency: Currency) 로 구현할수도 있겠습니다. 관심 있으신 분은 github.com/lambdista/money 를 참조하시면 됩니다.

Shapeless

Shapeless 는 많은 기능을 가지고 있기 때문에 여기서 모든걸 설명하긴 어렵고, 위에서 사용한 Generic, Aux 에 대해 간단히 소개만 하겠습니다. (관심 있으신 분은 Shapeless - Feature 2.0.0 를 참조하시면 됩니다.)

// https://github.com/milessabin/shapeless/blob/master/core/src/main/scala/shapeless/generic.scala

> load.ivy("com.chuusai" %% "shapeless" % "2.2.5")

> import shapeless._
import shapeless._

> case class Cat(name: String, catAge: Double)
defined class Cat  
> Generic[Cat]
res4: Generic[Cat] {  
  type Repr = 
    shapeless.::[String,shapeless.::[Double,shapeless.HNil]]
} = ...

Generic[A]Path-Dependent Type 으로 Repr 을 가지고 있습니다. 이는 A 에 따라 달라지는 값인데, 보통 R 로 표기합니다.

// https://github.com/milessabin/shapeless/blob/master/core/src/main/scala/shapeless/generic.scala#L103

trait Generic[T] extends Serializable {  
  /** The generic representation type for {T}, which will be composed of {Coproduct} and {HList} types  */
  type Repr

  /** Convert an instance of the concrete type to the generic value representation */
  def to(t : T) : Repr

  /** Convert an instance of the generic representation to an instance of the concrete type */
  def from(r : Repr) : T
}

Generic.Aux[A, R]Generic[A]ReprR 을 사용하는것으로, Generic[A] { type Repr = R } 과 동일합니다.

// https://github.com/milessabin/shapeless/blob/master/core/src/main/scala/shapeless/generic.scala#L148

object Generic {  
  ...

  type Aux[T, Repr0] = Generic[T] { type Repr = Repr0 }

  ...
}

Generic.Aux[A, R] 을 이용하면, 타입수준의 표현 R 과 실제 타입 Aisomorphic 변환을 수행할 수 있습니다. 위에서 봤던 tofrom 기억 하시죠?

만약 R 이 기본적인 타입이어서, Generic.Aux[A, R] 이 Shapeless 에서 자동 생성해 줄 경우 Currency 예제에서 보았듯이 implicit 로 가져오면, 바로 이용할 수 있습니다.

primitive 는 물론 case classGeneric[Cat] 처럼 자동생성되어 바로 가져다 쓸 수 있습니다. 중첩된것두 가능하구요.

> case class EnhancedCat(catType: String, cat: Cat)
defined class EnhancedCat

> Generic[EnhancedCat]
res6: Generic[EnhancedCat] {  
  type Repr = shapeless.::[String,shapeless.::[cmd3.Cat,shapeless.HNil]]
} = ...

여기서 HList 는 (Heterogenous List) 여러 타입을 담을 수 있는 리스트입니다.

이제 tofrom 예제를 보면

> val c1 = Cat("odie", 1.0)
c1: Cat = Cat("odie", 1.0)

> Generic[Cat].to(c1)
res9: String :: Double :: HNil = ::("odie", ::(1.0, HNil))

> val reconstructed = Generic[Cat].from(res9)
reconstructed: Cat = Cat("odie", 1.0)

> case class Dog(name: String, dogAge: Double)
defined class Dog

> val d1 = Dog("dog odie", 1.0)
d1: Dog = Dog("dog odie", 1.0)

> Generic[Dog].to(d1)
res13: String :: Double :: HNil = ::("dog odie", ::(1.0, HNil))

> val reconstructedFromDog = Generic[Cat].from(res13)
reconstructedFromDog: Cat = Cat("dog odie", 1.0)  

metaplasm.us - Type Classes and Generic Derivation 에서는 Shapeless 를 이용해서 문자열로부터 case class 를 자동생성하는 파서를 만드는 법을 보여줍니다.

CaseClassParser 가 있을 때, 문자열 "odie, 1.2"Dog 로 파싱하기 위해 CaseClassParser[Dog]("odie, 1.2") 처럼 쓰고싶다고 하면,

// ref - https://meta.plasm.us/posts/2015/11/08/type-classes-and-generic-derivation/

object CaseClassParser {  
  import shapeless._

  trait Parser[A] {
    def apply(s: String): Option[A]
  }

  def apply[A](s: String)(implicit P: Parser[A]): Option[A] = P(s)
}

이 때 shapeless.Generic[A] 를 이용하면 위에서 보았듯이 AHList 로 (Heterogenous List) 로 변경할 수 있으므로 Parser[HList] 만 있으면 됩니다.

HListList 처럼 consnil 로 구성되어 있습니다. HNilHList 파서를 만들면,

// ref - https://meta.plasm.us/posts/2015/11/08/type-classes-and-generic-derivation/

object CaseClassParser {  
  ...

  implicit val hnilParser = new Parser[HNil] {
    override def apply(s: String): Option[HNil] =
      if (s.isEmpty) Some(HNil) else None
  }

  implicit def hlistParser[H : Parser, T <: HList : Parser] = new Parser[H :: T] {
    override def apply(s: String): Option[H :: T] =
      s.split(",").toList match {
        case cell +: rest /* use `+:` instead of :: */ => for {
          head <- implicitly[Parser[H]].apply(cell)
          tail <- implicitly[Parser[T]].apply(rest.mkString(","))
        } yield head :: tail
      }
  }
}

그리고 implicitly[Parser[H]] 에서 사용할 개별 타입별 파서를 만들면

// ref - https://meta.plasm.us/posts/2015/11/08/type-classes-and-generic-derivation/

object CaseClassParser {  
  ...

  implicit val intParser = new Parser[Int] {
    override def apply(s: String): Option[Int] = Try(s.toInt).toOption
  }

  implicit val stringParser = new Parser[String] {
    override def apply(s: String): Option[String] = Some(s)
  }

  implicit val doubleParser = new Parser[Double] {
    override def apply(s: String): Option[Double] = Try(s.toDouble).toOption
  }
}

마지막으로, case classHList 로 만들어줄 caseClassParser 만 만들면 됩니다.

// ref - https://meta.plasm.us/posts/2015/11/08/type-classes-and-generic-derivation/

object CaseClassParser {  
  ...

  implicit def caseClassParser[C, R <: HList]
  (implicit G: Generic.Aux[C, R], reprParser: Parser[R]): Parser[C] = new Parser[C] {
    override def apply(s: String): Option[C] = reprParser.apply(s).map(G.from)
  } 
}

reprParser.apply(s)Option[R] 이므로 G.from 을 이용해 변환해주면 됩니다.

Previous Posts

References

]]>
<![CDATA[Easy Scalaz 4, Yoneda and Free Monad]]>

Free[F, A] 를 이용하면 Functor F 를 Monad 인스턴스로 만들 수 있습니다. 그런데, Coyoneda[G, A] 를 이용하면 아무 타입 G 나 Functor 인스턴스로 만들 수 있으므로 어떤 타입이든 (심지어 방금 만든 case class 조차) 모나드 인스턴스로 만들 수 있습니다.

Free 를 이용하면 사용자는 자신만의 Composable DSL 을 구성하고,

]]>
http://1ambda.github.io/easy-scalaz-4-yoneda-and-free-monad/4bdf7e2f-ba28-4630-80d2-b44b3953a5a8Sun, 06 Dec 2015 08:34:34 GMT

Free[F, A] 를 이용하면 Functor F 를 Monad 인스턴스로 만들 수 있습니다. 그런데, Coyoneda[G, A] 를 이용하면 아무 타입 G 나 Functor 인스턴스로 만들 수 있으므로 어떤 타입이든 (심지어 방금 만든 case class 조차) 모나드 인스턴스로 만들 수 있습니다.

Free 를 이용하면 사용자는 자신만의 Composable DSL 을 구성하고, 구성한 모나딕 연산을 실행하는 해석기를 작성하게 됩니다. 즉, 연산의 생성연산의 실행 을 분리하여 다루게 됩니다. 이는 side-effect 를 실행 시점으로 미룰 수 있다는 뜻입니다. (실행용 해석기와 별도로 테스트용 해석기를 작성하는 것도 가능합니다)

그러면, 제가 가장 좋아하는 Programs as Values: Fure Functional JDBC Programming 예제로 시작해보겠습니다.

If We Have a Monad

JDBC 를 쌩으로 사용한다면, 다음과 같은 코드를 작성해야 할텐데

// ref - http://tpolecat.github.io/

case class Person(name: String, age: Int)

def getPerson(rs: ResultSet): Person {  
  val name = rs.getString(1)
  val age  = rs.getInt(2)
}

다음과 같은 문제점이 있습니다.

  • managed resourceResultSet 을 프로그래머가 다룰 수 있습니다. 어디에 저장이라도 하고 나중에 사용한다면 문제가 될 수 있습니다.
  • rs.get*side-effect 를 만들어 내므로 테스트하기 쉽지 않습니다.

접근 방식을 바꿔보는건 어떨까요? 프로그램을 실행해서 side-effect 를 즉시 만드는 대신

  • 어떤 연산을 수행할지를 case class 로 만들고 이것들을 조합해 어떤 연산을 수행할지 나타낸뒤에
  • 연산의 조합을 번역해 실행하는 해석기(interpreter) 를 만들어 보겠습니다.

먼저 연산부터 정의하면,

sealed trait ResultSetOp[A]

final case class GetString(index: Int) extends ResultSetOp[String]  
final case class GetInt(index: Int)    extends ResultSetOp[Int]  
final case object Next                 extends ResultSetOp[Boolean]  
final case object Close                extends ResultSetOp[Unit]  

이 때 만약 ResultSetOp[A] 가 모나드라면 다음과 같이 작성할 수 있습니다.

def getPerson: ResultSetOp[Person] = for {  
  name <- GetString(1)
  age  <- GetInt(2)
} yield Person(name, age)

// Application Operation `*>`  (e.g `1.some *> 2.some== 2.some)
// See, http://eed3si9n.com/learning-scalaz/Applicative.html
def getNextPerson: ResultSetOp[Person] =  
  Next *> getPerson

def getPeople(n: Int): ResultSet[List[Person]] =  
  getNextPerson.repicateM(n) // List.fill(n)(getNextPerson).sequence

def getAllPeople: ResultSetIO[Vector[Person]] =  
  getPerson.whileM[Vector](Next)

ResultSetIO 는 모나드가 아니므로 위와 같이 작성할 수 없습니다.

Writing Your own DSL

놀랍게도, ResultSetIO 를 모나드로 만들 수 있습니다. flatMap, unit 구현 없이 얻을 수 있는 공짜 모나드입니다. 방법은 이렇습니다.

  • Free[F[_], ?]Functor F 에 대해 Monad 입니다
  • Coyoneda[S[_], ?] 는 아무 타입 S 에 대해 Functor 입니다.

따라서 Free[Coyoneda[S, A], A 는 아무 타입 S 에 대해서 모나드입니다.

import scalaz.{Free, Coyoneda}, Free._

// ResultSetOpCoyo is the Functor
type ResultSetOpCoyo[A] = Coyoneda[ResultSetOp, A] 

// ResultSetIO is the Monad
type ResultSetIO[A] = Free[ResultSetOpCoyo, A]

// same as
// type ResultSetIO2[A] = Free[({ type λ[α] = Coyoneda[ResultSetOp, α]})#λ, A]

따라서 다음처럼 작성할 수 있습니다.

val next                 : ResultSetIO[Boolean] = Free.liftFC(Next)  
def getString(index: Int): ResultSetIO[String]  = Free.liftFC(GetString(index))  
def getInt(index: Int)   : ResultSetIO[Int]     = Free.liftFC(GetInt(index))  
def close                : ResultSetIO[Unit]    = Free.liftFC(Close)  

여기서 Free.listFC 는 타입 ResultSetOp 를 바로 ResultSetIO 로 리프팅 해주는 헬퍼 함수입니다. (F = Free, C = Coyoneda)

// https://github.com/scalaz/scalaz/blob/series/7.2.x/core/src/main/scala/scalaz/Free.scala#L30

/** A version of `liftF` that infers the nested type constructor. */
def liftFU[MA](value: => MA)(implicit MA: Unapply[Functor, MA]): Free[MA.M, MA.A] =  
  liftF(MA(value))(MA.TC)

/** A free monad over a free functor of `S`. */
def liftFC[S[_], A](s: S[A]): FreeC[S, A] =  
    liftFU(Coyoneda lift s)

liftFU[MA] 에서, MA = Coyoneda[ResultSetOp, A] 로 보면 Free[MA.M, MA.A]Free[Coyoneda[ResultSetOp, A], A] 가 됩니다. (Unapply.scala)

이를 이용해서 get* 를 작성해 보면

import scalaz._, Scalaz._

def getPerson: ResultSetIO[Person] = for {  
  name <- getString(1)
  age  <- getInt(2)
} yield Person(name, age)

def getNextPerson: ResultSetIO[Person] =  
  next *> getPerson

def getPeople(n: Int): ResultSetIO[List[Person]] =  
  getNextPerson.replicateM(n) // List.fill(n)(getNextPerson).sequence

def getPersonOpt: ResultSetIO[Option[Person]] =  
  next >>= {
    case true  => getPerson.map(_.some)
    case false => none.point[ResultSetIO]
  }

def getAllPeople: ResultSetIO[Vector[Person]] =  
  getPerson.whileM[Vector](next)

DSL Interpreter

이제 RestSetOp 로 작성한 연산 (일종의 프로그램) 을 실행하려면, ResetSetOp 명령(case class) 을, 로직(side-effect 를 유발할 수 있는) 으로 변경해야 합니다.

NaturalTransformation 을 이용할건데, F ~> GFG 로 변경하는 변환(Transformation) 을 의미합니다.

// https://github.com/scalaz/scalaz/blob/series/7.2.x/core/src/main/scala/scalaz/package.scala#L113

/** A [[scalaz.NaturalTransformation]][F, G]. */
type ~>[-F[_], +G[_]] = NaturalTransformation[F, G]

// https://github.com/scalaz/scalaz/blob/series/7.2.x/core/src/main/scala/scalaz/NaturalTransformation.scala#L14
/** A universally quantified function, usually written as `F ~> G`,
  * for symmetry with `A => B`.
  *
  * Can be used to encode first-class functor transformations in the
  * same way functions encode first-class concrete value morphisms;
  * for example, `sequence` from [[scalaz.Traverse]] and `cosequence`
  * from [[scalaz.Distributive]] give rise to `([a]T[A[a]]) ~>
  * ([a]A[T[a]])`, for varying `A` and `T` constraints.
  */
trait NaturalTransformation[-F[_], +G[_]] {  
  self =>
  def apply[A](fa: F[A]): G[A]

  def compose[E[_]](f: E ~> F): E ~> G = new (E ~> G) {
    def apply[A](ea: E[A]) = self(f(ea))
  }

  def andThen[H[_]](f: G ~> H): F ~> H =
    f compose self
}

이제, ResultSetOpIO 로 변경하는 해석기를 작성하면, (Learning Scalaz - IO)

import scalaz.effect._

private def interpret(rs: ResultSet) = new (ResultSetOp ~> IO) {  
    def apply[A](fa: ResultSetOp[A]): IO[A] = fa match {
      case Next         => IO(rs.next)
      case GetString(i) => IO(rs.getString(i))
      case GetInt(i)    => IO(rs.getInt(i))
      case Close        => IO(rs.close)
      // more... 
    }
}

def run[A](a: ResultSetIO[A], rs: ResultSet): IO[A] =  
  Free.runFC(a)(interpret(rs))

Why Free?

Free 가 제공하는 가치는 다음과 같습니다. (Ref - StackExchange)

  • It is a lightweight way of creating a domain-specific language that gives you an AST, and then having one or more interpreters to execute the AST however you like
  • The free monad part is just a handy way to get an AST that you can assemble using Haskell's standard monad facilities (like do-notation) without having to write lots of custom code. This also ensures that your DSL is composable
  • You could then interpret this however you like: run it against a live database, run it against a mock, just log the commands for debugging or even try optimizing the queries

즉, Free 는 우리는 자신만의 Composable 한 DSL 을 구축하고, 필요에 따라 이 DSL 다른 방식으로 해석할 수 있도록 도와주는 도구입니다.

Free

(FreeYoneda 는 난해할 수 있으니, Free 를 어떻게 사용하는지만 알고 싶다면 Reasonably Priced Monad 로 넘어가시면 됩니다.)

어떻게 FFunctor 이기만 하면 Free[F[_], ?] 가 모나드가 되는걸까요? 이를 알기 위해선, 모나드가 어떤 구조로 이루어져 있는지 알 필요가 있습니다.

Monad

A monad is just a monoid in the category of endofunctors, what's the problem?

의사양반 이게 무슨소리요!

이제 MonoidFunctor 가 무엇인지 알아봅시다.

Monoid

어떤 집합 S 에 대한 닫힌 연산 *, 집합 내의 어떤 원소 e 가 다음을 만족할 경우 모노이드라 부릅니다.

  • e * a = a = a * e (identity)
  • (a * b) * c = a * (b * c) (associativity)

일반적으로 e 를 항등원이라 부릅니다. Option[A]None 을 항등원으로 사용하고, associativity 를 만족하는 A 의 연산을 사용하면 모노이드입니다. 따라서 A 가 모노이드면 Option[A] 도 모노이드입니다. (활용법은 Practical Scalaz 참조)

> load.ivy("org.scalaz" % "scalaz-core_2.11" % "7.2.0-M5")
> import scalaz._, Scalaz._


> 1.some |+| 2.some
res11: Option[Int] = Some(3)  
> 1.some |+| none
res12: Option[Int] = Some(1)  
> none[Int] |+| 1.some
res13: Option[Int] = Some(1)  

Functor

Functor 는 일반적으로 다음처럼 정의되는데, 이는 Functor FF 에서 값을 꺼내, 함수를 적용해 값을 변경할 수 있다는 것을 의미합니다.

A functor may go from one category to a different one

trait Functor[F[_]] {  
  def map[A, B](fa: F[A])(f: A => B): F[B]
}

그리고 Functoridentity function 을 항등원으로 사용하면, 모노이드입니다.

  • F.map(x => x) == F
  • F map f map g == F map (f compose g)

이 때, 변환의 인풋과 아웃풋이 같은 카테고리라면 이 Functorendo-functor 라 부릅니다.

A functor may go from one category to a different one, an endofunctor is a functor for which start and target category are the same.

Monad

그럼 다시 처음 문장으로 다시 돌아가면,

Monads are just monoids in the category of endofunctors

이 것의 의미를 이해하려면 모나드가 무엇인지 알아야 합니다.

trait Monad[F[_]] {  
  def point[A](a: A): F[A]
  def join[A](ffa: F[F[A]): F[A]
  ...
}

일반적으로는 point (=return) 와 bind (= flatMap) 으로 모나드를 정의하나, join, map 으로도 bind 를 정의할 수 있습니다.

// http://www.functionalvilnius.lt/meetups/meetups/2015-04-29-functional-vilnius-03/freemonads.pdf

trait Monad[F[_]] {  
  def point[A](a: A): F[A]
  def bind[A, B](fa: F[A])(f: A => F[B]): F[B]

  def map[A, B](fa: F[A])(f: A => B): F[B] = 
    bind(fa)(a => point(f(a))    
  def join[A](ffa: F[F[A]): F[A] = 
    bind(ffa)(fa => fa)
}

trait Monad[F[_]] {  
  def map[A, B](fa: F[A])(f: A => B): F[B]
  def point[A](a: A): F[A]
  def join[A](ffa: F[F[A]): F[A] /* flatten*/

  def bind[A, B](fa: F[A])(f: A => F[B]): F[B] = 
    join(map(fa)(f))
}

map, point, join 관점에서 모나드를 바라보면,

  • (endo)functor T : X → X
  • natural transformation μ : T × T → T (where × means functor composition (also known as join in Haskell)
  • natural transformation η : I → T (where I is the identity endofunctor on X also known as return in Haskell)

이때 위 연산들이 모노이드 법칙을 만족합니다.

  • e * a = a = a * e (identity)
  • (a * b) * c = a * (b * c) (associativity)

  • μ(η(T)) = T = μ(T(η)) (identity)

  • μ(μ(T × T) × T)) = μ(T × μ(T × T)) (associativity)

스칼라 코드로 보면,

> import scalaz._, Scalaz._

> val A = List(1, 2)
List[Int] = List(1, 2)

// identity left-side: μ(η(T)) = T
> A.map(x => Monad[List].point(x)).flatten
List[Int] = List(1, 2)

// identity right-side: μ(T(η)) = T
> Monad[List].point(A).flatten
List[Int] = List(1, 2)

// associativity
> val T = List(1, 2, 3, 4)
T: List[Int] = List(1, 2, 3, 4)  
> val TT = T.map(List(_))
TT: List[List[Int]] = List(List(1), List(2), List(3), List(4))

// associativity left-side: μ(μ(T × T) × T))
> TT.flatten.map(List(_))
res30: List[List[Int]] = List(List(1), List(2), List(3), List(4))  
> TT.flatten.map(List(_)).flatten
res31: List[Int] = List(1, 2, 3, 4)

// associativity right-side: μ(T × μ(T × T))
> List(TT.flatten)
res34: List[List[Int]] = List(List(1, 2, 3, 4))  
> List(TT.flatten).flatten
res35: List[Int] = List(1, 2, 3, 4)  

따라서 Monad(endo)Functor 카테고리에 대한 Monoid 입니다.

Free Monoid

Free Monadbind, point 에 대한 구현 없이, 모나드가 되듯이 Free Monoid 또한 연산과 항등원에 대한 구현 없이 구조적 으로 모노이드입니다.

항등원과 연산을 Zero, Append 라는 이름으로 구조화 하면,

// http://www.functionalvilnius.lt/meetups/meetups/2015-04-29-functional-vilnius-03/freemonads.pdf

sealed trait FreeMonoid[+A]  
final case object Zero extends FreeMonoid[Nothing]  
final case class Value[A](a: A) extends FreeMonoid[A]  
final case class Append[A](l: FreeMonoid[A], r: FreeMonoid[A]) extends FreeMonoid[A]  

모노이드는 associativity 를 만족하므로, Append 를 우측 결합으로 바꾸고, Zero 로 끝나도록 하면

// http://www.functionalvilnius.lt/meetups/meetups/2015-04-29-functional-vilnius-03/freemonads.pdf

sealed trait FreeMonoid[+A]  
final case object Zero extends FreeMonoid[Nothing]  
final case class Append[A](l: A, r: FreeMonoid[A]) extends FreeMonoid[A]  

List 와 동일한 구조임을 알 수 있습니다. 실제로, 리스트는 concatenation 연산, Nil 항등원에 대해 모노이드입니다.

Free Monad

이제까지의 내용을 정리하면

  • Monad is a monoid of functors
  • Then, Free Monad is a free Monoid of functors

따라서 Free MonadFunctorList 라 볼 수 있습니다.

모나드의 point, join구조화 (타입화) 하면,

def point[A](a: A): F[A]  
def join[A, B](ffa: F[F[A]): F[A]

sealed trait Free[F[_], A]  
case class Point[F[_], A](a: A) extends Free[F, A]             // == Return  
case class Join[F[_], A](ffa: F[Free[F, A]]) extends Free[F, A] // == Suspend  

map 을 타입화 하는 대신, FFunctor 라면 다음처럼 Free.point, Free.flatMap 을 작성할 수 있습니다.

sealed trait Free[F[_], A] {  
  def point[F[_]](a: A): Free[F, A] = Point(a)
  def flatMap[B](f: A => Free[F, B])(implicit functor: Functor[F]): Free[F, B] =
    this match {
      case Point(a)  => f(a)
      case Join(ffa) => Join(ffa.map(fa => fa.flatMap(f)))
    }
  def map[B](f: A => B)(implicit functor: Functor[F]): Free[F, B] =
    flatMap(a => Point(f(a)))
}

case class Point[F[_], A](a: A) extends Free[F, A]  
case class Join[F[_], A](ff: F[Free[F, A]]) extends Free[F, A]  

fa.flatMap(f) 의 결과가 Free[F, B]ffa.map 의 결과로 들어가므로, ffa.map(_ flatMap f) 의 결과는 F[Free[F, B] 입니다. 이걸 Free[F, B] 로 바꾸려면 Join 을 이용하면 됩니다.

이런 이유에서, FFunctorFree[F, A]Monad 입니다.

이제 리프팅과 실행을 위한 헬퍼 함수를 만들면,

// http://www.functionalvilnius.lt/meetups/meetups/2015-04-29-functional-vilnius-03/freemonads.pdf

import scalaz.{Functor, Monad, ~>}

def liftF[F[_], A](a: => F[A])(implicit F: Functor[F]): Free[F, A] =  
  Join(F.map(a)(Point[F, A]))

def foldMap[F[_], M[_], A](fm: Free[F, A])(f: F ~> M)  
                          (implicit FI: Functor[F], MI: Monad[M]): M[A] = 
  fm match {
    case Point(a) => MI.pure(a)
    case Join(ffa) => MI.bind(f(ffa))(fa => foldMap(fa)(f))
  }

여기서 F ~> MFM 으로 변환해주는, NaturalTransformation 입니다.

// http://www.functionalvilnius.lt/meetups/meetups/2015-04-29-functional-vilnius-03/freemonads.pdf

type ~>[-F[_], +G[_]] = NaturalTransformation[F, G] 

trait NaturalTransformation[-F[_], +G[_]] {  
  self =>
  def apply[A](fa: F[A]): G[A]

  def compose[E[_]](f: E ~> F): E ~> G = new (E ~> G) {
    def apply[A](ea: E[A]) = self(f(ea))
  }
}

MI.bind(f(ffa)) 의 결과는 M[Free[F, A]] 이므로 여기에서 bind (= flatMap) 로 fa 를 얻어, 재귀적으로 foldMap 을 호출합니다.

Scalaz Free Implementation

def flatMap[B](f: A => Free[F, B])(implicit functor: Functor[F]): Free[F, B] =  
    this match {
      case Point(a)  => f(a)
      case Join(ffa) => Join(ffa.map(fa => fa.flatMap(f)))
    }

Scalaz 에서는 flatMap 호출시 Stack 비용이 생각보다 크므로, flatMap 자체도 타입화하고 있습니다. 즉, Stack 대신에 Heap 을 사용합니다.

Point 대신, Return, Join 대신 Suspend, FlatMap 대신 GoSub 라는 타입 이름으로 구현되어 있습니다. (이해를 돕기 위해 7.x 대신, 6.0.4 버전을 차용)

// https://github.com/scalaz/scalaz/blob/release/6.0.4/core/src/main/scala/scalaz/Free.scala

final case class Return[S[+_], +A](a: A) extends Free[S, A]  
final case class Suspend[S[+_], +A](a: S[Free[S, A]]) extends Free[S, A]  
final case class Gosub[S[+_], A, +B](a: Free[S, A],  
                                     f: A => Free[S, B]) extends Free[S, B]
sealed trait Free[S[+_], +A] {  
  final def map[B](f: A => B): Free[S, B] =
    flatMap(a => Return(f(a)))

  final def flatMap[B](f: A => Free[S, B]): Free[S, B] = this match {
    case Gosub(a, g) => Gosub(a, (x: Any) => Gosub(g(x), f))
    case a           => Gosub(a, f)
  }
}

Trampoline

Free 를 이용하면, Stackoverflow 를 피할 수 있습니다. 이는 FreeflatMap 체인에서 스택 대신 힙을 이용하는 것을 응용한 것인데요,

// https://github.com/scalaz/scalaz/blob/release/6.0.4/core/src/main/scala/scalaz/Free.scala

/** A computation that can be stepped through, suspended, and paused */
type Trampoline[+A] = Free[Function0, A]  

이때 Function0Functor 이므로,

implicit Function0Functor: Functor[Function0] = new Functor[Function0] {  
  def fmap[A, B](f: A => B)(fa: Function0[A]): Function0[B] = 
    () => f(fa)
}

Free[Function0, A] 도 모나드입니다.

이제 스칼라에서 스택오버플로우가 발생하는 mutual recursion 코드를 만들어 보면,

// http://www.functionalvilnius.lt/meetups/meetups/2015-04-29-functional-vilnius-03/freemonads.pdf

def isOdd(n: Int): Boolean = {  
  if (0 == n) false
  else isEven(n -1)
}

def isEven(n: Int): Boolean = {  
  if (0 == n) true
  else isOdd(n -1)
}

isOdd(10000) // stackoverflow  

이제 Trampoline 을 이용하면

// http://www.functionalvilnius.lt/meetups/meetups/2015-04-29-functional-vilnius-03/freemonads.pdf

import scalaz._, Scalaz._, Free._

def isOddT(n: Int): Trampoline[Boolean] =  
  if (0 == n) return_(false)
  else suspend(isEvenT(n - 1))

def isEvenT(n: Int): Trampoline[Boolean] =  
  if (0 == n) return_(true)
  else suspend(isOddT(n - 1))

scala> isOddT(2000000).run  
res7: Boolean = false

scala> isOddT(2000001).run  
res8: Boolean = true  

return_suspend 는 다음처럼 정의되어 있습니다.

// https://github.com/scalaz/scalaz/blob/series/7.2.x/core/src/main/scala/scalaz/Free.scala#L15

trait FreeFunctions {

  ...
  def return_[S[_], A](value: => A)(implicit S: Applicative[S]): Free[S, A] =
    Suspend[S, A](S.point(Return[S, A](value)))

  def suspend[S[_], A](value: => Free[S, A])(implicit S: Applicative[S]): Free[S, A] =
    Suspend[S, A](S.point(value))

Yoneda, Coyoneda

포스트의 시작 부분에서 Coyoneda 에 대한 언급을 기억하시나요?

  • Free[F[_], ?]Functor F 에 대해 Monad 입니다
  • Coyoneda[S[_], ?] 는 아무 타입에 대해 Functor 입니다.

Coyoneda 가 어떻게 Functor 를 만들어내는지 확인해 보겠습니다. 이 과정에서 dualYoneda 도 같이 살펴보겠습니다. (같은 Category 내에서, morphism 방향만 다른 경우)

먼저, Yoneda, Coyoneda 의 기본적인 내용을 훑고 가면

  • Yoneda, CoyonedaFunctor 입니다
  • Yoneda[F[_], A], Coyoneda[F[_], A]F[A]isomorphic 입니다 (FFunctor 일 경우)
  • Yoneda[F, A] 에서 F[A] 로의 homomorphismFFunctor 가 아닐 경우에도 존재합니다
  • F[A] 에서 Coyoneda[F, A] 로의 homomorphismFFunctor 가 아닐 경우에도 존재합니다 (중요)
  • Yoneda, Coyoneda 모두 Functor 가 필요한 시점을 미루고, Functor.map 의 체인을, 일반 함수의 체인으로 표현합니다. 결국엔 Functor 가 필요합니다 (중요)


(Image - http://evolvingthoughts.net/2010/08/homology-and-analogy/)

Coyoneda[F[_], A]F 와 상관없이 Functor 인 이유는, F[A] -> Coyoenda[F[_], A] 로의 변환이 F Functor 인 것과 상관이 없으며 Coyoneda 자체가 Functor 인스턴스이기 때문입니다.

추상은 간단합니다. Functor[F]F[A] -> F[B] 로의 변환을 f: A => B 만 가지고 해 낼 수 있다는 점을 역이용하면 됩니다. F[A]Functor.map(f) 를 적용하는 것이 아니라, 값 A 가 있을 때 f(a) 를 적용한 뒤에, F[B] 를 만들면 됩니다. 다시 말해

  • Functor[F]F[A]f: A => B, g: B = > C 가 가 있을 때 Functor[F].map(f compose g) 대신
  • f compose g 를 먼저 하고, 이것의 결과값인 C 를 이용해 F[C] 를 만들면 됩니다. 그러면 Functor[F].map 연산을 함수의 컴포지션으로 해결할 수 있습니다.

Yoneda

// https://github.com/scalaz/scalaz/blob/series/7.1.x/core/src/main/scala/scalaz/Yoneda.scala

abstract class Yoneda[F[_], A] { yo =>  
  def apply[B](f: A => B): F[B]

  def run: F[A] = apply(a => a)

  def map[B](f: A => B): Yoneda[F, B] = new Yoneda[F, B] {
    override def apply[C](g: (B) => C): F[C] = yo(f andThen g)
  }
}

/** `F[A]` converts to `Yoneda[F, A]` for any functor `F` */
def apply[F[_]: Functor, A](fa: F[A]): Yoneda[F, A] = new Yoneda[F, A] {  
  override def apply[B](f: A => B): F[B] = Functor[F].map(fa)(f)
}

/** `Yoneda[F, A]` converts to `F[A` for any `F` */
def from[F[_], A](yo: Yoneda[F, A]): F[A] =  
  yo.run

/** `Yoneda[F, _]` is a functor for any `F` */
implicit def yonedaFunctor[F[_]]: Functor[({ type  λ[α] = Yoneda[F,α]})#λ] =  
  new Functor[({type λ[α] = Yoneda[F, α]})#λ] {
    override def map[A, B](ya: Yoneda[F, A])(f: A => B): Yoneda[F, B] =
      ya map f
  }

Yoneda[F[_], ?] 는 그 자체로 Functor 이나 이를 만들기 위해선 FFunctor 여야 합니다. 반면 Yoneda[F, A] -> F[A] 로의 변환은 FFunctor 이던 아니던 상관 없습니다.

Coyoneda

그렇다면, dualCoyoneda 는 어떨까요? Yoneda F[A]Functor 로 부터 얻는것이 아니라, Identity 를 이용해, 처음부터 F[A] 를 가지고 있습니다. 이로 부터 얻어지는 결론은 놀랍습니다.

sealed abstract class Coyoneda[F[_], A] { coyo =>  
  type I
  val fi: F[I]
  val k: I => A

  final def map[B](f: A => B): Aux[F, I, B] =
    apply(fi)(f compose k)

  final def run(implicit F: Functor[F]): F[A] =
    F.map(fi)(k)
}

type Aux[F[_], A, B] = Coyoneda[F, B] { type I = A }

def apply[F[_], A, B](fa: F[A])(_k: A => B): Aux[F, A, B] =  
  new Coyoneda[F, B] {
    type I = A
    val k = _k
    val fi = fa
  }

/** `F[A]` converts to `Coyoneda[F, A]` for any `F` */
def lift[F[_], A](fa: F[A]): Coyoneda[F, A] = apply(fa)(identity[A])

/** `Coyoneda[F, A]` converts to `F[A]` for any Functor `F` */
def from[F[_], A](coyo: Coyoneda[F, A])(implicit F: Functor[F]): F[A] =  
  F.map(coyo.fi)(coyo.k)

/** `CoyoYoneda[F, _]` is a functor for any `F` */
implicit def coyonedaFunctor[F[_]]: Functor[({ type  λ[α] = Coyoneda[F,α]})#λ] =  
  new Functor[({type λ[α] = Coyoneda[F, α]})#λ] {
    override def map[A, B](ca: Coyoneda[F, A])(f: A => B): Coyoneda[F, B] =
      ca.map(f)
  }

따라서 Coyoneda[F[_], ?] 를 만들기 위해서 FFunctor 일 필요가 없습니다.

Stackoverflow - The Power of (Co)yoneda 에선 다음처럼 설명합니다.

newtype Yoneda f a = Yoneda { runYoneda :: forall b . (a -> b) -> f b }

instance Functor (Yoneda f) where  
  fmap f y = Yoneda (\ab -> runYoneda y (ab . f))

data CoYoneda f a = forall b . CoYoneda (b -> a) (f b)

instance Functor (CoYoneda f) where  
  fmap f (CoYoneda mp fb) = CoYoneda (f . mp) fb

So instead of appealing to the Functor instance for f during definition of the Functor instance for Yoneda, it gets "defered" to the construction of the Yoneda itself. Computationally, it also has the nice property of turning all fmaps into compositions with the "continuation" function (a -> b).

The opposite occurs in CoYoneda. For instance, CoYoneda f is still a Functor whether or not f is. Also we again notice the property that fmap is nothing more than composition along the eventual continuation.

So both of these are a way of "ignoring" a Functor requirement for a little while, especially while performing fmaps.

Reasonably Priced Monad

for comprehension 내에서는 단 하나의 모나드 밖에 쓸 수 없습니다. 단칸방 세입자 모나드 Monad Transformer 등을 사용하긴 하는데 불편하기 짝이 없지요.

Rúnar BjarnasonComposable application architecture with reasonably priced monads 에서 Coproduct 를 이용해 Free 를 조합하는 법을 소개합니다. (이 비디오는 꼭 보셔야합니다!)

요약하면 Free 를 이용해 생성한 서로 다른 두개의 모나드는 같은 for comprehension 내에서 사용할 수 없습니다. 이 때 Coproduct 를 이용해서 하나의 타입으로 묶고, 타입 자동 주입을 위해 Inject 를 이용하면 많은 코드 없이도, 편리하게 Free 를 이용할 수 있다는 것입니다.

예를 들어 다음과 처럼 두개의 프리 모나드 Interact, Auth 가 있을 때

// Interact
trait InteractOp[A]  
final case class Ask(prompt: String) extends InteractOp[String]  
final case class Tell(msg: String)   extends InteractOp[Unit]

type CoyonedaInteract[A] = Coyoneda[InteractOp, A]  
type Interact[A] = Free[CoyonedaInteract, A]

def ask(prompt: String) = liftFC(Ask(prompt))  
def tell(msg: String) = liftFC(Tell(msg))  
// Auth
case class User(userId: UserId, permissions: Set[Permission])

sealed trait AuthOp[A]  
final case class Login(userId: UserId, password: Password) extends AuthOp[Option[User]]  
final case class HasPermission(user: User, permission: Permission) extends AuthOp[Boolean]

type CoyonedaAuth[A] = Coyoneda[AuthOp, A]  
type Auth[A] = Free[CoyonedaAuth, A]

def login(userId: UserId, password: Password): FreeC[F, Option[User]] =  
  liftFC(Login(userId, password))

def hasPermission(user: User, permission: Permission): FreeC[F, Boolean] =  
  liftFC(HasPermission(user, permission))
// Log

sealed trait LogOp[A]  
final case class Warn(message: String)  extends LogOp[Unit]  
final case class Error(message: String) extends LogOp[Unit]  
final case class Info(message: String)  extends LogOp[Unit]

type CoyonedaLog[A] = Coyoneda[LogOp, A]  
type Log[A] = Free[CoyonedaLog, A]

object Log {  
  def warn(message: String)  = liftFC(Warn(message))
  def info(message: String)  = liftFC(Info(message))
  def error(message: String) = liftFC(Error(message))

다음처럼 같은 for comprehension 구문에서 사용할 수 없습니다.

// doesn't compile

for {  
  userId <- ask("Insert User ID: ")
  password <- ask("Password: ")
  user <- login(userId, password)
  _ <- info(s"user $userId logged in")
  hasPermission <- user.cata(
    none = point(false),
    some = hasPermission(_, "scalaz repository")
  )
  _ <- warn(s"$userId has no permission for scalaz repository")
} yield hasPermission

이 때 Coproduct 를 이용하면, 가능합니다.

// combine free monads
type Language0[A] = Coproduct[InteractOp, AuthOp, A]  
type Language[A] = Coproduct[LogOp, Language0, A]  
type LanguageCoyo[A] = Coyoneda[Language, A]  
type LanguageMonad[A] = Free[LanguageCoyo, A]  
def point[A](a: => A): FreeC[Language, A] = Monad[LanguageMonad].point(a)

// combine interpreters
val interpreter0: Language0 ~> Id = or(InteractInterpreter, AuthInterpreter)  
val interpreter: Language ~> Id = or(LogInterpreter, interpreter0)

// run a program
def main(args: Array[String]) {  
  def program(implicit I: Interact[Language], A: Auth[Language], L: Log[Language]) = {
    import I._, A._, L._

    for {
      userId <- ask("Insert User ID: ")
      password <- ask("Password: ")
      user <- login(userId, password)
      _ <- info(s"user $userId logged in")
      hasPermission <- user.cata(
        none = point(false),
        some = hasPermission(_, "scalaz repository")
      )
      _ <- warn(s"$userId has no permission for scalaz repository")
    } yield hasPermission
  }

  program.mapSuspension(Coyoneda.liftTF(interpreter))
}

여기서 orlift 는 라이브러리 코드라 생각하시면 됩니다. 이제 변화된 프리 모나드 부분을 보면,

object Auth {  
  type UserId = String
  type Password = String
  type Permission = String

  implicit def instance[F[_]](implicit I: Inject[AuthOp, F]): Auth[F] =
    new Auth
}

class Auth[F[_]](implicit I: Inject[AuthOp, F]) {  
  import Common._
  def login(userId: UserId, password: Password): FreeC[F, Option[User]] =
    lift(Login(userId, password))

  def hasPermission(user: User, permission: Permission): FreeC[F, Boolean] =
    lift(HasPermission(user, permission))
}

class Interact[F[_]](implicit I: Inject[InteractOp, F]) {  
  import Common._

  def ask(prompt: String): FreeC[F, String] =
    lift(Ask(prompt))

  def tell(message: String): FreeC[F, Unit] =
    lift(Tell(message))
}

object Interact {  
  implicit def instance[F[_]](implicit I: Inject[InteractOp, F]): Interact[F] =
    new Interact
}

class Log[F[_]](implicit I: Inject[LogOp, F]) {  
  import Common._

  def warn(message: String)  = lift(Warn(message))
  def info(message: String)  = lift(Info(message))
  def error(message: String) = lift(Error(message))
}

object Log {  
  implicit def instant[F[_]](implicit I: Inject[LogOp ,F]) =
    new Log
}

이제, Common 을 보면

object Common {  
  import scalaz.Coproduct, scalaz.~>

  def or[F[_], G[_], H[_]](f: F ~> H, g: G ~> H): ({type cp[α] = Coproduct[F,G,α]})#cp ~> H =
    new NaturalTransformation[({type cp[α] = Coproduct[F,G,α]})#cp,H] {
      def apply[A](fa: Coproduct[F,G,A]): H[A] = fa.run match {
        case -\/(ff) ⇒ f(ff)
        case \/-(gg) ⇒ g(gg)
      }
    }

  def lift[F[_], G[_], A](fa: F[A])(implicit I: Inject[F, G]): FreeC[G, A] =
    Free.liftFC(I.inj(fa))
}

Coproduct[F, G, A]둘 중 하나 를 의미하는 추상입니다. 결과로 F[A] \/ G[A] (scalaz either) 을 돌려줍니다.

// https://github.com/scalaz/scalaz/blob/series/7.2.x/core/src/main/scala/scalaz/Coproduct.scala

final case class Coproduct[F[_], G[_], A](run: F[A] \/ G[A]) {  
  ...
}

trait CoproductFunctions {  
  def leftc[F[_], G[_], A](x: F[A]): Coproduct[F, G, A] =
    Coproduct(-\/(x))

  def rightc[F[_], G[_], A](x: G[A]): Coproduct[F, G, A] =
    Coproduct(\/-(x))

  ...
}

Inject[F[_], G[_]]F, G 를 포함하는 더 큰 타입인 Coproduct 를 만들때 쓰입니다.

def lift[F[_], G[_], A](fa: F[A])(implicit I: Inject[F, G]): FreeC[G, A] =  
  Free.liftFC(I.inj(fa))

// F == Langauge
class Log[F[_]](implicit I: Inject[LogOp, F]) {  
  def warn(message: String)  = lift(Warn(message))
  def info(message: String)  = lift(Info(message))
  def error(message: String) = lift(Error(message))
}

Inject 는 이렇게 생겼습니다.

// https://github.com/scalaz/scalaz/blob/series/7.2.x/core/src/main/scala/scalaz/Inject.scala

sealed abstract class Inject[F[_], G[_]] {  
  def inj[A](fa: F[A]): G[A]
  def prj[A](ga: G[A]): Option[F[A]]
}

sealed abstract class InjectInstances {  
  implicit def reflexiveInjectInstance[F[_]] =
    new Inject[F, F] {
      def inj[A](fa: F[A]) = fa
      def prj[A](ga: F[A]) = some(ga)
    }

  implicit def leftInjectInstance[F[_], G[_]] =
    new Inject[F, ({type λ[α] = Coproduct[F, G, α]})#λ] {
      def inj[A](fa: F[A]) = Coproduct.leftc(fa)
      def prj[A](ga: Coproduct[F, G, A]) = ga.run.fold(some(_), _ => none)
    }

  implicit def rightInjectInstance[F[_], G[_], H[_]](implicit I: Inject[F, G]) =
      new Inject[F, ({type λ[α] = Coproduct[H, G, α]})#λ] {
        def inj[A](fa: F[A]) = Coproduct.rightc(I.inj(fa))
        def prj[A](ga: Coproduct[H, G, A]) = ga.run.fold(_ => none, I.prj(_))
      }
}

따라서 F, G 타입만 맞추어 주면 Inject 인스턴스는 자동으로 생성됩니다.

다음시간에는 side-effect 의 세계로 넘어가 ST, IO 등을 살펴보겠습니다.

Previous Posts

References

]]>
<![CDATA[Easy Scalaz 3, ReaderWriterState with Kleisli]]>

Composition (합성) 은 함수형 언어에서 중요한 테마중 하나인데요, 이번 시간에는 Kleisli 를 이용해 어떻게 함수를 타입으로 표현하고, 합성할 수 있는지 살펴보겠습니다. 그리고 나서, Reader, Writer 에 대해 알아보고, 이것들과 State 를 같이 사용하는 RWST 에 대해 알아보겠습니다.

Kleisli

State(S) => (S, A) 를 타입클래스로 표현한 것이라면, A =>

]]>
http://1ambda.github.io/easy-scalaz-3-readerwriterstate-with-kleisli/1be76d77-2344-4db8-b924-f9015146e001Tue, 17 Nov 2015 11:32:42 GMT

Composition (합성) 은 함수형 언어에서 중요한 테마중 하나인데요, 이번 시간에는 Kleisli 를 이용해 어떻게 함수를 타입으로 표현하고, 합성할 수 있는지 살펴보겠습니다. 그리고 나서, Reader, Writer 에 대해 알아보고, 이것들과 State 를 같이 사용하는 RWST 에 대해 알아보겠습니다.

Kleisli

State(S) => (S, A) 를 타입클래스로 표현한 것이라면, A => B 를 타입클래스로 표현한 것도 있지 않을까요? 그렇게 되면, 스칼라에서 지원하는 andThen, compose 을 이용해서 함수를 조합하는 것처럼, 타입 클래스를 조합할 수 있을겁니다. Kleisli 가 바로, 그런 역할을 하는 타입 클래스입니다.

Kleisli represents a function A => M[B]

타입을 보면, 단순히 A => B 이 아니라 A => M[B] 를 나타냅니다. 이는 KleisliM 을 해석하고, 조합할 수 있는 방법을 제공한다는 것을 의미합니다. 실제 구현을 보면,

// https://github.com/scalaz/scalaz/blob/series/7.2.x/core/src/main/scala/scalaz/Kleisli.scala#L8

final case class Kleisli[M[_], A, B](run: A => M[B]) { self =>  
  ...

  def >=>[C](k: Kleisli[M, B, C])(implicit b: Bind[M]): Kleisli[M, A, C] =  kleisli((a: A) => b.bind(this(a))(k.run))

  def andThen[C](k: Kleisli[M, B, C])(implicit b: Bind[M]): Kleisli[M, A, C] = this >=> k

  def >==>[C](k: B => M[C])(implicit b: Bind[M]): Kleisli[M, A, C] = this >=> kleisli(k)

  def andThenK[C](k: B => M[C])(implicit b: Bind[M]): Kleisli[M, A, C] = this >==> k

  /** alias for `compose` */
  def <=<[C](k: Kleisli[M, C, A])(implicit b: Bind[M]): Kleisli[M, C, B] = k >=> this

  def compose[C](k: Kleisli[M, C, A])(implicit b: Bind[M]): Kleisli[M, C, B] = k >=> this

  def <==<[C](k: C => M[A])(implicit b: Bind[M]): Kleisli[M, C, B] = kleisli(k) >=> this

  def composeK[C](k: C => M[A])(implicit b: Bind[M]): Kleisli[M, C, B] = this <==< k
  ...
}

Kleisli Example 에서 간단한 예제를 가져와서 사용법을 살펴보도록 하겠습니다.

// https://github.com/scalaz/scalaz/blob/series/7.2.x/example/src/main/scala/scalaz/example/KleisliUsage.scala

case class Continent(name: String, countries: List[Country] = List.empty)  
case class Country(name: String, cities: List[City] = List.empty)  
case class City(name: String, isCapital: Boolean = false, inhabitants: Int = 20)

val data: List[Continent] = List(  
  Continent("Europe"),
  Continent("America",
    List(
      Country("Canada",
        List(
          City("Ottawa"), City("Vancouver"))),
      Country("USA",
        List(
          City("Washington"), City("New York"))))),
  Continent("Asia",
    List(
      Country("India",
        List(City("New Dehli"), City("Calcutta"))))))

여기에 다음의 함수를 정의하면

def continents(name: String): List[Continent] =  
  data.filter(k => k.name.contains(name))

def countries(continent: Continent): List[Country] = continent.countries

def cities(country: Country): List[City] = country.cities

def save(cities: List[City]): Try[Unit] =  
  Try {
    // do IO or some side-effectful operations
    cities.foreach(c => println("Saving " + c.name))
  }

def inhabitants(c: City): Int = c.inhabitants  

이제 A => M[B] 형태의 여러 함수들을 만들었으므로 이를 Kleisli 를 이용해 조합할 수 있습니다. (이 예제에서 M == List)

// Kleisli[List, String, City]
val allCities = kleisli(continents) >==> countries >==> cities

// Kleisli[List, String, Int]
val cityInhabitants = allCities map inhabitants  

allCitiesString 을 인자로 받기도 하고, M == ListKleisli 기 때문에 List 를 인자로 받을 수도 있습니다. (=<<)

allCities("America") map(println)

// output
City(Ottawa,false,20)  
City(Vancouver,false,20)  
City(Washington,false,20)  
City(New York,false,20)

(allCities =<< List("America", "Asia")).map(println)

// output
City(Ottawa,false,20)  
City(Vancouver,false,20)  
City(Washington,false,20)  
City(New York,false,20)  
City(New Dehli,false,20)  
City(Calcutta,false,20)  

Kleisli 가 제공하는 함수를 다시 살펴보면,

def =<<(a: M[A])(implicit m: Bind[M]): M[B] = m.bind(a)(run)

def map[C](f: B => C)(implicit M: Functor[M]): Kleisli[M, A, C] =  
  kleisli(a => M.map(run(a))(f))

def mapK[N[_], C](f: M[B] => N[C]): Kleisli[N, A, C] =  
  kleisli(run andThen f)

def flatMapK[C](f: B => M[C])(implicit M: Bind[M]): Kleisli[M, A, C] =  
  kleisli(a => M.bind(run(a))(f))

def flatMap[C](f: B => Kleisli[M, A, C])(implicit M: Bind[M]): Kleisli[M, A, C] =  
  kleisli((r: A) => M.bind[B, C](run(r))(((b: B) => f(b).run(r))))

여기서 mapK :: M[B] => N[C] 를 이용하면 현재 Kleisli[M, _, _]Kleisli[N, _, _] 로 변경할 수 있습니다.

위에서 정의한 save 함수는 List[A] 를 받아 Try[Unit] 를 여기에 사용할 수 있습니다.

// Kleisli[Try, String, Unit]
val getAndSaveCities = allCities mapK save  

local 을 이용하면 함수를 prepend 할 수 있습니다.

// def local[AA](f: AA => A): Kleisli[M, AA, B] =
//   kleisli(f andThen run)

def index(i: Int): String = data(i).name

// Kleisli[List, Int, City]
val allCitiesWithIndex = allCities local index

allCitiesWithIndex(1) map(println)

// output
City(Ottawa,false,20)  
City(Vancouver,false,20)  
City(Washington,false,20)  
City(New York,false,20)  

Kleisli 에 대한 더 읽을거리는 아래 링크를 참조해주세요.

Reader

KleisliA => M[B] 를 나타낸다면, ReaderA => B (Function1) 를 의미하는 타입클래스입니다. 얼핏 생각하기에 Kleisli[Id, A, B] 일것 같죠? 실제 구현을 보면 (scalaz 에서 타입 얼라이어스는 package.scala 에 정의되어 있습니다.)

// https://github.com/scalaz/scalaz/blob/series/7.2.x/core/src/main/scala/scalaz/package.scala

type ReaderT[F[_], E, A] = Kleisli[F, E, A]  
val ReaderT = Kleisli  
type Reader[E, A] = ReaderT[Id, E, A]

object Reader {  
    def apply[E, A](f: E => A): Reader[E, A] = Kleisli[Id, E, A](f)
  }

ReaderKlelsli 이므로, Reader[A, B] >==> Reader[B, C]Reader[A, C] 가 됩니다. 게다가 KleisliflatMap 을 정의하고 있으므로 monadic composition 을 작성할 수 있습니다.

The point of a Reader is to supply some configuration object without having to manually (or implicitly) pass i around all the functions.

요는, 함수 사이의 체인을 엮어 새로운 함수를 만들수 있고 이로인해 직접 파라미터를 넘겨줄 필요가 없습니다. 예를 들어

type URI = String  
type Key = String  
type Value = String

val uri: Reader[Get, URI]  
val queryString: Reader[URI, String]  
val body: Reader[String, Map[Key, Value]

// Get => Map[Key, Value]
val queryStringToBody = uri >==> queryString >==> body  

간단히 구현을 해보겠습니다. 예외 처리는 외부에서 Try 혹은 \/.fromTryCatchThrowable 등으로 한다 가정하고 로직에만 집중해보면,

// model
trait HttpRequest {  
  def url: String
}
case class GET(url: String) extends HttpRequest  
case class POST(url: String, body: Map[String, String]) extends HttpRequest

val uri: Reader[GET, String] = Reader { req: GET => req.url }  
val queryString: Reader[String, String] = Reader { url: String => url.split("\\?")(1) }  
val body: Reader[String, Map[String, String]] = Reader { queries: String =>  
  val qs = queries.split("&").toList
  qs.foldLeft(Map.empty[String, String]) { (acc: Map[String, String], q) =>
    val kv = q.split("=")
    acc.updated(kv(0), kv(1))
  }
}

val queryStringToBody: Reader[GET, Map[String, String]] = uri >==> queryString >==> body  

queryStringToBody 를 사용해 보면,

val get1 = GET("http://www.google.com/search?query=scalaz&site=github")  
val post1 = POST("http://www.google.com/search", Map("query" -> "scalaz", "site" -> "github"))  
val post2 = POST("https://www.google.com/search", Map("query" -> "scalaz", "site" -> "github"))

queryStringToBody.run(get1) shouldBe Map("query" -> "scalaz", "site" -> "github")  

함수를 몇개 더 작성해보면,

val toHttpsRequest = Reader { url: String => url.replaceAll("http://$", "https://") }  
val sslProxy: Reader[_ >: readerwriterstate.HttpRequest, readerwriterstate.HttpRequest] = Reader { req: readerwriterstate.HttpRequest =>  
  req match {
    case request if request.url.startsWith("https://") => request
    case request: POST => request.copy(url = toHttpsRequest(request.url))
    case request: GET  => request.copy(url = toHttpsRequest(request.url))
  }
}

val convertGetToPost: Reader[_ >: readerwriterstate.HttpRequest, POST] = Reader { req : readerwriterstate.HttpRequest =>  
  req match {
    case get: GET =>
      val split = get.url.split("\\?")
      val (path, query) = (split(0), split(1))
      val postBody = body.run(query)

      POST(path, postBody)

    case post: POST => post
  }
}

이제 HttpRequest 서브타입을 받아, 프록시를 적용하고, GET 이면 POST 로 변경하는 함수를 조합해보면 아래와 같습니다.

(:>Type Bound 에 대해서는 Scala School - Type & PolymorphismScala School - Advanced Types 를 참조해주세요.)

val proxiedPost: Reader[_ >: HttpRequest, POST] = sslProxy >==> convertGetToPost

// spec
proxiedPost.run(get1) shouldBe post2  

flatMap for Reader

ReaderKleisli 고, 이것간의 합성은 >==> 을 이용한다는것을 확인했습니다. 그럼 flatMap 은 어디에 쓰는걸까요?

type ReaderT[F[_], E, A] = Kleisli[F, E, A]  
type Reader[E, A] = ReaderT[Id, E, A]

final case class Kleisli[M[_], A, B](run: A => M[B]) { self =>  
  ...

  // andThen
  def >=>[C](k: Kleisli[M, B, C])(implicit b: Bind[M]): Kleisli[M, A, C] =  kleisli((a: A) => b.bind(this(a))(k.run))

  def >==>[C](k: B => M[C])(implicit b: Bind[M]): Kleisli[M, A, C] = this >=> kleisli(k)

  def flatMapK[C](f: B => M[C])(implicit M: Bind[M]): Kleisli[M, A, C] =
    kleisli(a => M.bind(run(a))(f))

  def flatMap[C](f: B => Kleisli[M, A, C])(implicit M: Bind[M]): Kleisli[M, A, C] =
    kleisli((r: A) => M.bind[B, C](run(r))(((b: B) => f(b).run(r))))

  ...
}

flatMap 을 보면 재미난 점이 보입니다. Kleisli[M, A, B]Kleisli[M, A, C]flatMap 으로 엮는데, r: A 를 넣어서 run(r) 을 실행하는걸 보실 수 있습니다. Kleisli[M, A, C] 까지도요!

A 자체가 일종의 설정(Configuration) 값으로써 모든 Kleisli 에서 사용됩니다. 그렇기에

  • Reader[A, B]Reader[B, C]>==> 으로
  • Reader[A, B]Reader[A, C]flatMap 으로 엮을 수 있습니다.

Dependency Injection using Reader

Reader 를 이용하면 스칼라에서 별도의 라이브러리 없이 Dependency Injection (이하 DI) 를 구현할 수 있습니다. 이는 위에서 보았던 flatMap 의 특징을 이용하면 됩니다. 다음과 같은 모델이 있다고 할 때,

case class User(id: Long,  
                name: String,
                age: Int,
                email: String,
                supervisorId: Long)

trait UserRepository {  
  def get(id: Long): User
  def find(name: String): User
}

trait UserService {  
  def getUser(id: Long): Reader[UserRepository, User] =
    Reader(repo => repo.get(id))

  def findUser(userName: String): Reader[UserRepository, User] =
    Reader(repo => repo.find(userName))

  def getUserInfo(userName: String): Reader[UserRepository, Map[String, String]] = for {
    user <- findUser(userName)
    supervisor <- getUser(user.supervisorId)
  } yield Map(
    "email" -> s"${user.email}",
    "boss"  -> s"${supervisor.name}"
  )
}

다음처럼 주입할 수 있습니다.

object UserRepositoryDummyImpl extends UserRepository {  
  override def get(id: Long): User = ???
  override def find(name: String): User = ???
}

class UserApplication(userRepository: UserRepository) extends UserService  
object UserApplication extends UserApplication(UserRepositoryDummyImpl)  

이외에도 스칼라에서 언어 자체의 기능만으로 DI 를 구현하는 방법으로 Cake Pattern , Implicit 등이 있습니다. (Scala Dependency Injection using Reader 참조)

위의 두 방법과 Reader 를 사용한 방법을 비교하면,

  • Cake Pattern 에 비해 코드가 짧고
  • Implicit 를 이용하지 않으므로 함수 시그니쳐가 간단합니다.

Writer

Writer[W, A]run: (W, A) 을 값으로 가지는 case class 입니다. 재미난 점은, flatMap 을 이용해 두개의 Writer 를 엮으면 각각의 값인 (w1, a1), (w2, a2) 에 대해서 사용자가 다루는 값인 a1, a2 를 제하고 w1w2 가 일종의 State 처럼 관리되어 자동으로 append 된다는 점입니다. 따라서 많은 튜토리얼들이 logging 을 예로 들어 Writer 를 설명하곤 합니다.

test("WriterOps") {  
  val w1: Writer[String, Int] = 10.set("w1 created")
  val w2: Writer[String, Int] = 20.set("w2 created")

  val result: Writer[String, Int] = for {
    n1 <- w1
    n2 <- w2
  } yield n1 + n2

  // What if we use `List[String]` instead of `String`?
  result.run shouldBe ("w1 createdw2 created", 30)
}

Scalaz 구현을 보면

type Writer[W, A] = WriterT[Id, W, A]

final case class WriterT[F[_], W, A](run: F[(W, A)]) { self =>  
  ...

  def flatMap[B](f: A => WriterT[F, W, B])(implicit F: Bind[F], s: Semigroup[W]): WriterT[F, W, B] =
    flatMapF(f.andThen(_.run))

  def flatMapF[B](f: A => F[(W, B)])(implicit F: Bind[F], s: Semigroup[W]): WriterT[F, W, B] =
    writerT(F.bind(run){wa =>
      val z = f(wa._2)
      F.map(z)(wb => (s.append(wa._1, wb._1), wb._2))
    })

  ...

WriterT 에서 FId 라 하면 Writer 가 되고 flatMap 로직은 다음처럼 단순화 할 수 있습니다.

case class Writer[W, A](run: (W, A)) { self =>  
  def flatMap[B](f: A => Writer[W, B])(implicit s: Semigroup[W]) {
    val (w1, a) = self.run
    val (w2, b) = f(a)
    (s.append(w1, w2), b)
  }
}

여기서 Semigroup.scala 은, Associativity (결합법칙) 을 만족하는 binary operator 를 정의하는 타입 클래스입니다. (위에서 append)

// https://github.com/scalaz/scalaz/blob/series/7.1.x/core/src/main/scala/scalaz/Semigroup.scala#L55

 /**
   * A semigroup in type F must satisfy two laws:
    *
    *  - '''closure''': `∀ a, b in F, append(a, b)` is also in `F`. This is enforced by the type system.
    *  - '''associativity''': `∀ a, b, c` in `F`, the equation `append(append(a, b), c) = append(a, append(b , c))` holds.
   */
  trait SemigroupLaw {
    def associative(f1: F, f2: F, f3: F)(implicit F: Equal[F]): Boolean =
      F.equal(append(f1, append(f2, f3)), append(append(f1, f2), f3))
  }

Monoid 는 결합법칙을 만족하는 덧셈 연산과, 항등원 연산을 정의하는 타입 클래스인데, Scalaz 에서는 MonoidSemigroup 을 상속받습니다.

trait Monoid[F] extends Semigroup[F] { self =>  
  ...

https://raw.githubusercontent.com/1ambda/1ambda.github.io/master/assets/images/about-type-class/Typeclassopedia-diagram.png


따라서 Writer[W, A]flatMap 을 이용하기 위해서는 WSemigroup 여야 하고 그래야만 flatMap 내부에서 자동으로 Wappend 할 수 있습니다.

스칼라에서 제공하는 List 등의 기본 타입은 Scalaz 에서 Monoid 를 제공합니다. (scalaz.std.List, scalaz.std 참조)

정리하면, Writer[W, A] 를 이용하면 값인 A 를 조작하면서 W 를 신경쓰지 않고, 자동으로 append 시킬 수 있습니다. (e.g logging)

Writer Example

간단한 모델을 만들면,

import scalaz._, Scalaz._

trait ThreadState  
case object Waiting    extends ThreadState  
case object Running    extends ThreadState  
case object Terminated extends ThreadState  
case class Thread(tid: String, name: String, state: ThreadState)  
case class Process(pid: String, threads: List[Thread])

object Process {  
  type Logger[A] = Writer[Vector[String], A]

  def genRandomID: String = java.util.UUID.randomUUID().toString.replace("-", "")

  def createThread(name: String): Logger[Thread] = {
    val tid = genRandomID
    Thread(tid, name, Waiting).set(Vector(s"Thread [$tid] was created"))
  }

  def createEmptyProcess: Logger[Process] = {
    val pid = genRandomID
    Process(pid, Nil).set(Vector(s"Empty Process [$pid] was created"))
  }

  def createNewProcess: Logger[Process] = for {
    mainThread <- createThread("main")
    process <- createEmptyProcess
    _ <- Vector(s"Main Thread [${mainThread.tid}] was added to Process [${process.pid}").tell
  } yield process.copy(threads = mainThread.copy(state = Running) :: process.threads)
}

여기서 WList[String] 대신 Vector[String] 을 사용하는 이유는, append 가 더 빠르기 때문입니다. (Scala Collection Performance Characteristics 참조)

test("Writer usage2") {  
  import readerwriterstate.Process._

  val (written, process) = createNewProcess.run

  process.threads.length shouldBe 1
  process.threads.head.name shouldBe "main"

  /* map lets you map over the value side */
  val ts: Logger[List[Thread]] = createNewProcess.map(p => p.threads)
  ts.value.length shouldBe 1

  /* with mapWritten you can map over the written side */
  val edited: Vector[String] = createNewProcess.mapWritten(_.map { log => "[LOG]" + log }).written
  println(edited.mkString("\n"))

  /** output
   * [LOG]Thread [557ad5bd0f3b4d49bac85b05ebedcd7b] was created
   * [LOG]Empty Process [710bd940ebdd4a82b949a32b585a12d9] was created
   * [LOG]Main Thread [557ad5bd0f3b4d49bac85b05ebedcd7b] was added to Process [710bd940ebdd4a82b949a32b585a12d9]
   */

  /* with mapValue, you can map over both sides */
  createNewProcess.mapValue { case (log, p) =>
    (log :+ "Add an IO thread",
     p.copy(threads = Thread(genRandomID, "IO-1", Waiting) :: p.threads))
  }

  // `:++>` `:++>>`, `<++:`, `<<++:`
  createNewProcess :++> Vector("add some log")
  val emptyWithLog = createEmptyProcess :++>> { process =>
    Vector(s"${process.pid} is an empty process")
  }

   println(emptyWithLog.written)

  // output: Vector(Empty Process [cf211fc366ab4d20a0c25a27d173accd] was created, cf211fc366ab4d20a0c25a27d173accd is an empty process)

  // Writer is an applicative
  val emptyProcesses: Logger[List[readerwriterstate.Process]] =
    (createEmptyProcess |@| createEmptyProcess) { List(_) |+| List(_) }

  val ps = emptyProcesses.value
  ps.length shouldBe 2
}

Applicative Builder, WriterT Functions 를 참고하시면 이해가 더 쉽습니다.

RWST

ReaderWriterState 는 다름이 아니라, 이제까지 보았던 Reader, Writer, State 를 모두 이용하는 타입 클래스입니다. Reader 로 설정값을 읽고, Writer 로 중간 과정을 기록하고, State 로 상태를 변경 또는 유지해 가며 연산을 수행할 수 있습니다. Scalaz 에서는 예제로 ReaderWriterStateTUsage.scala 를 제공하고 있습니다.

이제까지 늘 그래왔듯이, ReaderWriterState[R, W, S, A] 또한 ReaderWriterStateT[Id, R, W, S, A]type alias 입니다. Reader, Writer, State 에서 사용했었던 함수들도 같이 제공됩니다.

type ReaderWriterState[-R, W, S, A] = ReaderWriterStateT[Id, R, W, S, A]  
type ReaderWriterStateT[F[_], -R, W, S, A] = IndexedReaderWriterStateT[F, R, W, S, S, A]

object ReaderWriterState extends ReaderWriterStateTInstances with ReaderWriterStateTFunctions {  
  def apply[R, W, S, A](f: (R, S) => (W, A, S)): ReaderWriterState[R, W, S, A] = IndexedReaderWriterStateT[Id, R, W, S, S, A] { (r: R, s: S) => f(r, s) }
}

apply 를 보면, ReaderWriterState 는 타입 (R, S) => (W, A, S) 함수를 넘겨주어 생성할 수 있습니다. Reader, State 를 받고, Writer, A (결과값), State 를 돌려주는 것으로 해석할 수 있습니다.

ReadwrWriterState.flatMapState, Writer, ReaderflatMap 을 모두 조합한것처럼 생겼습니다. 하는일도 그렇구요.

/** A monad transformer stack yielding `(R, S1) => F[(W, A, S2)]`. */
sealed abstract class IndexedReaderWriterStateT[F[_], -R, W, -S1, S2, A] {

  ...

  def flatMap[B, RR <: R, S3](f: A => IndexedReaderWriterStateT[F, RR, W, S2, S3, B])(implicit F: Bind[F], W: Semigroup[W]): IndexedReaderWriterStateT[F, RR, W, S1, S3, B] =
    new IndexedReaderWriterStateT[F, RR, W, S1, S3, B] {
      def run(r: RR, s1: S1): F[(W, B, S3)] = {
        F.bind(self.run(r, s1)) {
          case (w1, a, s2) => {
            F.map(f(a).run(r, s2)) {
              case (w2, b, s3) => (W.append(w1, w2), b, s3)
            }
          }
        }
      }
    }

  ...

RWST Example

예제를 위해 간단한 모델을 만들어 보겠습니다.

  • ReaderDatabaseConfig
  • WriterVector[String]
  • StateConnection 을 이용하고

결과값으로 타입 A 를 돌려주는 Task[A] 를 만들면 아래와 같습니다.

object Database {  
  type Task[A] = ReaderWriterState[DatabaseConfig, Vector[String] /* log */, Connection, A]
  ...

여기에 몇 가지 제약조건을 걸어보겠습니다.

  • DatabaseConfig.operationTimeoutMillis 에 의해서 타임아웃(OperationTimeoutException) 발생
  • OperationTimeoutException 발생시, 연산을 즉시 중단하고, 오류 없이 수행이 되었을 경우 commit
  • Post Commit Action 등록을 할 수 있어야 하며, commit 후 순차대로 자동 실행

이제 필요한 몇몇 클래스를 만들고

type Action = () => Unit  
case class PostCommitAction(id: String, action: Action)  
case class DatabaseConfig(operationTimeoutMillis: Long)  
case class ResultSet() /* dummy */

case class Connection(id: String,  
                      actions: List[PostCommitAction] = Nil) {

  def commit = {}
  def executeAndReturn(query: String): ResultSet = ResultSet()
  def execute(query: String): Unit = {}
}

class OperationTimeoutException private(ex: RuntimeException) extends RuntimeException(ex) {  
  def this(message:String) = this(new RuntimeException(message))
  def this(message:String, throwable: Throwable) = this(new RuntimeException(message, throwable))
}

object OperationTimeoutException {  
  def apply(message:String) = new OperationTimeoutException(message)
  def apply(message:String, throwable: Throwable) = new OperationTimeoutException(message, throwable)
}

이제 사용자가 API 를 사용하는 것을 한번 상상해보겠습니다. commit 이 어쨌건, 사용자가 하고싶은 일은 쿼리를 실행해서 결과값을 받아오거나, 필요한 post commit action 을 등록하는 일일겁니다. 나머지는 다 알아서 해주겠거니 하고 기대하고 있겠지요. 아래와 같은 API 가 있다면,

def createTask[A](f: Connection => A): Task[A]  
def addPostCommitAction(action: Action): Task[Unit]  
def run[A](task: Task[A]): Option[A]  

사용자들이 이런 방식으로 사용할 수 있습니다.

case class Person(name: String, address: Address)  
case class Address(street: String)

def getPerson(name: String): Task[Person] = createTask { conn =>  
  val rs: ResultSet = conn.executeAndReturn(s"SELECT * FROM USER WHERE name == '$name'")

  /* get a person using the result set */
  ...
}

def updateAddress(person : Person): Task[Unit] = createTask { conn =>  
  /* do something */
  conn.execute(
    s"UPDATE ADDRESS SET street = '${person.address.street}' where person_name = '${person.name}'")
}

val getAndUpdatePersonTask: Task[Person] = for {  
  p <- getPerson("1ambda")
  updatedP = p.copy(address = Address("BACON STREET 234"))
  _ <- addPostCommitAction(() => println("post commit action1"))
  _ <- updateAddress(updatedP)
  _ <- addPostCommitAction(() => println("post commit action2"))
} yield updatedP

val person: Option[Person] = Database.run(getAndUpdatePersonTask)  

이제 상상했던 함수를 구현해 보면,

// https://github.com/1ambda/scala/blob/master/learning-scalaz/src/main/scala/readerwriterstate/Database.scala

import java.util.UUID  
import scalaz._, Scalaz._  
import Database._  
import com.github.nscala_time.time.Imports._

object Database {

  ...
  object Implicit {
    implicit def defaultConnection: Connection = Connection(genRandomUUID)
    implicit def defaultConfig = DatabaseConfig(500)
  }

  private def genRandomUUID: String = UUID.randomUUID().toString

  private def execute[A](f: => A, conf: DatabaseConfig): A = {
    val start = DateTime.now

    val a = f

    val end = DateTime.now

    val time: Long = (start to end).millis

    if (time > conf.operationTimeoutMillis)
      throw OperationTimeoutException(s"Operation timeout: $time millis")

    a
  }

  def createTask[A](f: Connection => A): Task[A] =
    ReaderWriterState { (conf, conn) =>
      val a = execute(f(conn), conf)
      (Vector(s"Task was created with connection[${conn.id}]"), a, conn)
    }

  def addPostCommitAction(action: Action): Task[Unit] =
    ReaderWriterState { (conf, conn: Connection) =>

      val postCommitAction = PostCommitAction(genRandomUUID, action)
      (Vector(s"Add PostCommitAction(${postCommitAction.id})"),
        Unit,
        conn.copy(actions = conn.actions :+ postCommitAction))
    }

  def run[A](task: Task[A])
            (implicit defaultConf: DatabaseConfig, defaultConn: Connection): Option[A] = {

    \/.fromTryCatchThrowable[(Vector[String], A, Connection), Throwable](
      task.run(defaultConf, defaultConn)
    ) match {
      case -\/(t) =>
        println(s"Operation failed due to ${t.getMessage}") /* logging */
        none[A]

      case \/-((log: Vector[String], a: A, conn: Connection)) =>
        conn.commit /* close connection */

        log.foreach { text => println(s"[LOG] $text")} /* logging */

        /* run post commit actions */
        conn.actions foreach { _.action() }

        a.some
    }
  }

이제 실제로 500 ms 를 초과하는 연산을 실행하면, 예외가 발생하는 것을 확인할 수 있습니다.

  test("Database example") {

    val slowQuery: Task[Person] = createTask { conn =>
      sleep(600)
      Person("Sherlock", Address("BACON ST 221-B"))
    }

    val getPeopleTask: Task[List[Person]] = for {
      p1 <- getPerson("Mycroft")
      p2 <- getPerson("Watson")
      p3 <- slowQuery
      _ <- addPostCommitAction(() => println("post commit1"))
    } yield p1 :: p2 :: p3 :: Nil

    import Database.Implicit._
    val people = Database.run(getPeopleTask)

    // log: Operation failed due to java.lang.RuntimeException: Operation timeout: 603 millis
    people shouldBe None
}

Previous Posts

References

]]>
<![CDATA[About Type Class]]>

프로그래머가 하는 행위를 극도로 단순화해서 표현하면 저수준 의 데이터를 고수준 데이터로 변환하는 일입니다.

여기서 저수준이란, Stream, Byte, JSON, String 등 현실세계의 데이터를, 고수준이라 함은 비즈니스 로직, 제약조건 등이 추가된 도메인 객체, 모델 등 데이터를 말합니다.

이로 인해

  1. 저수준을 고수준으로 변환하는건 조건이 충족되지 않은 데이터와 연산 과정에서 일어나는 시스템 오류를 처리해야하기

]]>
http://1ambda.github.io/about-type-class/40c5a8ff-eb1f-40a3-a8b6-38f5e97ced9fTue, 20 Oct 2015 16:43:43 GMT

프로그래머가 하는 행위를 극도로 단순화해서 표현하면 저수준 의 데이터를 고수준 데이터로 변환하는 일입니다.

여기서 저수준이란, Stream, Byte, JSON, String 등 현실세계의 데이터를, 고수준이라 함은 비즈니스 로직, 제약조건 등이 추가된 도메인 객체, 모델 등 데이터를 말합니다.

이로 인해

  1. 저수준을 고수준으로 변환하는건 조건이 충족되지 않은 데이터와 연산 과정에서 일어나는 시스템 오류를 처리해야하기 때문에 힘든일입니다

  2. 갖은 고생 끝에 데이터를 고수준으로 끌어올린 뒤에야, 그 데이터를 프로그래머 자신의 세상에서 마음껏 주무를 수 있습니다

  3. 프로그래머가 작업을 끝낼 시점이 되면, 데이터를 저수준으로 변환해서 저장 또는 전송해야 하는데, 이미 제약조건이 충족 되었기 때문에 이는 손쉬운 일입니다

따라서 핵심은 다음의 두가지 입니다.

  • 쉽게 고수준으로 변환할 수 있는가 (연산)
  • 변환된 고수준 데이터가 얼마나 다루기 편한가 (추상)

프로그래머가 적절한 연산 을 선택하면 힘들이지 않고 변환을 해낼것이고, 적절한 추상 (혹은 모델링) 을 한다면 직관적인 코드로 데이터를 주무를 수 있게 되는데, 이 것을 도와주는 것이 바로 타입 클래스 입니다.

타입 클래스를 이용하면,

  • if null 을 Option 으로,
  • S => (S, A) 을 State[S, A] 로
  • if if if 를 Applicative 로
  • fail-slow, fail-fast 로직은 ValidationNel 과 Either 로
  • F[G[A]]G[F[A]] 로의 변경은 Traversal 로
  • setC{applyB{getA}} 를 getA > applyB > setC 로(Kleisli) 표기할 수 있습니다.

이렇게 연산을 각각의 타입으로 표시하기 때문에 로직을 파악하고, 분할하기 쉽습니다. 그리고 연산을 작성하는 과정이 타입을 조합하는 과정과 동일하기 때문에 컴파일러의 도움을 받을수 있구요.

타입클래스는 연산이 어떠해야 하는지 를 다루기 때문에 연산을 조합할 수 있는 다양한 함수들이 포함되어 있습니다. 이것을 이용하면 직관적인 방식으로 데이터를 다룰 수 있는데, 예를 들어 다음은 코드 실행과정에서 예외 발생 시에만 롤백을 수행하고, 예외를 돌려주는 코드입니다. (간략화 하였습니다.)

\/.fromTryCatch { 
  val result = runQuery; 
  commit; 
  result 
} leftMap(err => rollback; err};

만약 if null 보다 Option 을 쓰는것이 더 편하고 익숙하다면, Applicative 부터 천천히 시작해보는건 어떨까요?

Reference

]]>
<![CDATA[Easy Scalaz 2, Monad Transformer]]>

지난 시간엔 State Monad 를 다루었습니다. 그러나 State 만 이용해서는 유용한 프로그램을 작성할 수 없습니다. 우리가 다루는 연산은 Option, Future 등 다양한 side-effect 가 필요하기 때문인데요,

서로 다른 Monad 를 조합할 수 있다면 좋겠지만, 아쉽게도 Functor, Applicative 와 달리 모나드는 composing 이 불가능합니다. Monad Do Not Compose

여러 모나드를 조합해서

]]>
http://1ambda.github.io/easy-scalaz-2-monad-transformer/20ace812-f624-48ad-bfe9-9d1b707ed231Fri, 16 Oct 2015 14:46:45 GMT

지난 시간엔 State Monad 를 다루었습니다. 그러나 State 만 이용해서는 유용한 프로그램을 작성할 수 없습니다. 우리가 다루는 연산은 Option, Future 등 다양한 side-effect 가 필요하기 때문인데요,

서로 다른 Monad 를 조합할 수 있다면 좋겠지만, 아쉽게도 Functor, Applicative 와 달리 모나드는 composing 이 불가능합니다. Monad Do Not Compose

여러 모나드를 조합해서 사용하려면 Monad Transformer 가 필요합니다.

Monad transformers are useful for enabling interaction between different types of monads by "nesting" them into a higher-level monadic abstraction.

Monad Transformer 란 여러 모나드의 effect 를 엮어 새로운 모나드를 만들때 쓸 수 있습니다. 예를 들어

  • 어떤 임의의 모나드 M 을 사용하면서 State 효과를 주고 싶을 때 StateT 를 이용할 수 있습니다
  • State 를 다루면서, for 내에서 Option 처럼 로직을 다루고 싶다면, OptionT[State, A] 를 이용할 수 있습니다

대략 감이 오시죠? (State 에 대한 자세한 설명은 Easy Scalaz 1 - State 을 참조)

scalaz 에는 기본적으로 여러 모나드 트랜스포머가 정의되어 있습니다. (scalaz.core.*) ListT, MaybeT 등등. 이번 글에서는 아래 3개의 모나드 트랜스포머만 다룰 예정입니다.

The Problem

모나드 트랜스포머를 설명하기 위해, 사용자의 Github Repository 에 어느 언어가 쓰였는지를 알려주는 findLanguage 함수를 작성해보겠습니다.

// ref - https://softwarecorner.wordpress.com/2013/12/06/scalaz-optiont-monad-transformer/

import scalaz._, Scalaz._

case class User(name: String, repositories: List[Repository])  
case class Repository(name: String, languages: List[Language])  
case class Language(name: String, line: Long)

object GithubService {  
  def findLanguage(users: List[User],
                    userName: String,
                    repoName: String, 
                    langName: String): Option[Language] =
    for {
      u <- users          find { _.name === userName }
      r <- u.repositories find { _.name === repoName }
      l <- r.languages    find { _.name === langName }
    } yield l
}

List[User] 를 받아 해당 유저의 레포지토리에서 특정 언어가 있는지, 없는지를 검사하는 간단한 함수입니다.

val u1 = User(  
  "1ambda", List(
    Repository("akka", List(
      Language("scala", 4990),
      Language("java",  12801)
    )),

    Repository("scalaz", List(
      Language("scala", 1451),
      Language("java",  291)
    ))
  )
)

val u2 = User(  
  "2ambda", List()
)

val users = List(u1, u2)

// spec
"findLanguage" in {
  val l1 = findLanguage(users, "1ambda", "akka", "scala")
  val l2 = findLanguage(users, "1ambda", "akka", "haskell")
  val l3 = findLanguage(users, "1ambda", "rx-scala", "scala")
  val l4 = findLanguage(users, "adbma1", "rx-scala", "scala")

  l1.isDefined shouldBe true
  l2.isDefined shouldBe false
  l3.isDefined shouldBe false
  l4.isDefined shouldBe false
  }

그런데, 요구사항이 갑자기 변경되어 많이 쓰이는 언어도 찾아내야 합니다. 검사한 것 중 1000 줄이 넘는 언어리스트를 상태로 다루면,

type LangState = State[List[Language], Option[Language]]  

이제 findLanguage 를 다시 작성하면,

def findLanguage2(users: List[User],  
                  userName: String,
                  repoName: String,
                  langName: String): LangState =
  for {
    u <- users.find(_.name === userName).point[LangState]
    r  <- u.repositories.find(_.name === repoName).point[LangState]
    l <- r.languages.find(_.name === langName).point[LangState]
    _ <- modify(langs: List[Language] => if (l.line >= 1000) l :: langs else langs)
  } yield song

당연히 컴파일이 되지 않습니다. 이는 u, r, l 이 각각 User, Repository, Language 가 아니라 Option[User], Option[Repository], Option[Language] 이기 때문입니다. 패턴 매칭을 적용하면 아래와 같은 코드가 만들어집니다.

def findLanguage(users: List[User],  
                  userName: String,
                  repoName: String,
                  langName: String): LangState[Option[Language]] =
  for {
    optUser <- (users.find { _.name === userName }).point[LangState]
    optRepository <- (
      optUser match {
        case Some(u) => u.repositories.find(_.name === repoName)
        case None => none[Repository] // same as Option.empty[Repository]
      }).point[LangState]
    optLanguage <- (optRepository match {
      case Some(r) => r.languages.find(_.name === langName)
      case None    => none[Language]
    }).point[LangState]
    _ <- modify { langs: List[Language] => optLanguage match {
      case Some(l) if l.line => 1000 => l :: langs
      case _                         => langs
    }}
  } yield optLanguage

위 코드에서 중복되는 부분을 발견할 수 있는데요, 바로 State[S, Option[A]] 에 대해 매번 패턴 매칭을 수행하는 부분이 중복입니다. 이를 제거하기 위해 새로운 모나드 LangStateOption 을 만들면

case class LangStateOption[A](run: LangState[Option[A]])  

이제 모나드를 구현하면

implicit val LangStateOptionMonad = new Monad[LangStateOption] {  
  override def point[A](a: => A): LangStateOption[A] =
    LangStateOption(a.point[Option].point[LangState])

  override def bind[A, B](fa: LangStateOption[A])(f: (A) => LangStateOption[B]): LangStateOption[B] =
    LangStateOption(fa.run.flatMap { (o: Option[A]) => o match {
      case Some(a) => f(a).run
      case None    => (none[B]).point[LangState] /* same as `(None: Option[B]).point[LangState]` */
    }})
}

// findLanguage impl
def findLanguage3(users: List[User],  
                  userName: String,
                  repoName: String,
                  langName: String): LangStateOption[Language] =
  for {
    u <- LangStateOption((users.find { _.name === userName }).point[LangState])
    r <- LangStateOption((u.repositories.find { _.name === repoName }).point[LangState])
    l <- LangStateOption((r.languages.find { _.name === langName }).point[LangState])
    _ <- LangStateOption((modify { langs: List[Language] =>
      if (l.line >= 1000) l :: langs else langs
    }) map (_ => none[Language]))
  } yield l

여기서 잘 보셔야 할 두 가지 부분이 있습니다

A. 우리가 임의의 모나드와 Option 을 엮은 새로운 모나드를 생성한다면, LangStateOption 타입만 다르고 모두 동일한 형태의 코드를 가지게 됩니다.

그런고로 scalaz 에서는 Option 과 임의의 모나드 M 을 조합한 타입을 OptionT[M[_], A] 로 제공합니다.

B. StateOption 을 엮어서 State[S, Option[A]] 를 엮을 경우 State 가 먼저 실행되고, 그 후에야 Option 이 효과를 발휘합니다. (fa.run.flatMap { o => ...}

따라서 어떤 모나드 트랜스포머와, 모나드를 엮냐에 따라서 의미가 달라집니다. 예를 들어 scalaz 에서 제공해주는 모나드 트랜스포머 OptionTStateT 에 대해

  • OptionT[LangState, A]run: LangState[Option[A]] 이기 때문에 optional value 를 돌려주는 state action 을 의미하고
  • 반면 StateT[Option, List[Language], A]]run: Option[State[List[Language], A]] 기 때문에 존재하지 않을 수 있는 (None) 일 수 있는 state action 을 의미합니다

MonadTrans

지금까지 우리가 했던 일을 살펴보면,

M[A] -> M[N[A]] -> NT[M[N[_]], A]

즉 하나의 모나드 M 이 있을때 AN[A]lifting 하는 N 을 위한 모나드 트랜스포머를 NT 를 정의했습니다. scalaz 에서 사용된 모나드 트랜스포머 구현인 MonadTrans, OptionT 을 보면 다음과 같습니다.

// https://github.com/scalaz/scalaz/blob/series/7.2.x/core/src/main/scala/scalaz/MonadTrans.scala
trait MonadTrans[F[_[_], _]] {  
  def liftM[G[_]: Monad, A](g: G[A]): F[G, A]

  ...
}

// OptionT `liftM` implementation (F == Option)
// https://github.com/scalaz/scalaz/blob/series/7.2.x/core/src/main/scala/scalaz/OptionT.scala#L213

def liftM[G[_], A](a: G[A])(implicit G: Monad[G]): OptionT[G, A]) =  
  OptionT[G, A](G.map[A, Option[A]](a) { (a: A) => 
    a.point[Option]
  }

Monad Transformer 또한 Monad 기 때문에 또 다른 Monad Transformer 와 중첩이 가능합니다. 예를 들어

// ref - http://www.slideshare.net/StackMob/monad-transformers-in-the-wild
type VIO[A] = ValidationT[IO, Throwable, A]  
def doIO: VIO[Option[String]  
val r = OptionT[VIO, String] = optionT[VIO](doIO)

// OptionT[ValidationT[IO, Throwable, A]
// == IO[Validation[Throwable, Option[A]]

OptionT

이제 모나드 트랜스포머가 무엇인지 알았으니, OptionT 를 사용해 볼까요?

// https://github.com/scalaz/scalaz/blob/series/7.2.x/core/src/main/scala/scalaz/OptionT.scala

final case class OptionT[F[_], A](run: F[Option[A]]) {  
  self =>

  private def mapO[B](f: Option[A] => B)(implicit F: Functor[F]) = F.map(run)(f)

  def map[B](f: A => B)(implicit F: Functor[F]): OptionT[F, B] = new OptionT[F, B](mapO(_ map f))

  def flatMap[B](f: A => OptionT[F, B])(implicit F: Monad[F]): OptionT[F, B] = new OptionT[F, B](
    F.bind(self.run) {
      case None    => F.point(None: Option[B])
      case Some(z) => f(z).run
    }
  )

  def flatMapF[B](f: A => F[B])(implicit F: Monad[F]): OptionT[F, B] = new OptionT[F, B](
    F.bind(self.run) {
      case None    => F.point(none[B])
      case Some(z) => F.map(f(z))(b => some(b))
    }
  )

OptionT 는 두 가지 방법으로 생성할 수 있습니다.

  • val ma: M[A] 가 있을 때 ma.liftM[OptionT]
  • val oa: Option[A] 가 있을 때 OptionT(oa.point[M])
// type LangState[A] = State[List[Language], A]
val l = Language("lisp", 309)  
val os1: OptionT[LangState, Language] = l.point[LangState].liftM[OptionT]  
val os2: OptionT[LangState, Language] = OptionT(l.some.point[LangState])

os1 === os2  
os1.run === os2.run  
os1.run.runZero[List[Language]] === os2.run.runZero[List[Language]]  

이제 findLanguage 함수를 OptionT 로 작성할 수 있습니다.

def findLanguage(users: List[User],  
                  userName: String,
                  repoName: String,
                  langName: String): OptionT[LangState, Language] =
  for {
    u <- OptionT((users.find { _.name === userName }).point[LangState])
    r <- OptionT((u.repositories.find { _.name === repoName }).point[LangState])
    l <- OptionT((r.languages.find { _.name === langName }).point[LangState])
    _ <- modify { langs: List[Language] =>
      if (l.line >= 1000) l :: langs else langs
    }.liftM[OptionT]
  } yield l

Sequencing OptionT

findLanguage 를 이용해서, findLanguages 를 작성하는 것이 가능할까요?

case class LanguageLookup(userName: String, repoName: String, langName: String)

// Option[List[Language]] 를 돌려주는 All or Nothing 버전
def findLanguages(users: List[User],  
                     lookups: List[LanguageLookup]): OptionT[LangState, List[Language]] = ???

// List[Option[Language]] 를 돌려주는 덜 엄격한 버전
def findLanguages(users: List[User],  
                     lookups: List[LanguageLookup]): LangState[List[Option[Language]]] = ???

일단 OptionT[LangState, List[Language]] 를 돌려주는 것 부터 작성해 보겠습니다.

def findLanguages1(users: List[User],  
                   lookups: List[LanguageLookup]): OptionT[LangState, List[Language]] =
  lookups map { lookup =>
    findLanguage(users, lookup.userName, lookup.repoName, lookup.langName)
  }

// compile error
Error:(87, 13) type mismatch;

 found   : List[scalaz.OptionT[LangState, Language]]
 required: scalaz.OptionT[LangState,List[Language]]
    lookups map { lookup =>
            ^

우리는 OptionT[LangState, List[Language]] 를 돌려줘야 하는데, 단순히 map 만 적용해서는 List[OptionT[LangState, Language]] 밖에 못 얻습니다. 따라서 Traversable.traverseU 를 이용하면

def findLanguages1(users: List[User],  
                   lookups: List[LanguageLookup]): OptionT[LangState, List[Language]] =
  lookups.traverseU { lookup =>
    findLanguage(users, lookup.userName, lookup.repoName, lookup.langName)
  }

여기서 traverseU(f) 가 하는 일은

  • map(f): 함수 f 를 적용합니다.
  • List[OptionT[LangState, Language]]OptionT[LangState, List[Language]] 를 변환합니다. Option 모나드의 효과를 적용하면서요 (sequence)

일반적으로 F[G[B]]G[F[B]] 로 변경하는 함수를 sequence 라 부릅니다. (FMonad, Gapplicative)

final def sequence[G[_], B](implicit ev: A === G[B], G: Applicative[G]): G[F[B]] = {  
  ...
}

mapsequence 를 호출하는 함수가 바로 위에서 보았던 traverse 입니다. 그런데, 더 높은 추상에서 보면 방금 말했던 것과는 반대로, sequenceidentity 함수를 maptraverse 입니다. scalaz 에도 실제로 이렇게 구현되어 있습니다.

// https://github.com/scalaz/scalaz/blob/series/7.2.x/core/src/main/scala/scalaz/syntax/TraverseSyntax.scala#L25

  final def traverse[G[_], B](f: A => G[B])(implicit G: Applicative[G]): G[F[B]] =
    G.traverse(self)(f)

  /** Traverse with the identity function */
  final def sequence[G[_], B](implicit ev: A === G[B], G: Applicative[G]): G[F[B]] = {
    val fgb: F[G[B]] = ev.subst[F](self)
    F.sequence(fgb)
  }

위에서 traverse 가 아니라 traverseU 를 호출한 이유는 OptionT 에 대한 타입추론을 이용하기 위해서 입니다.


이제 덜 엄격한 findLanguages 함수를 작성해보겠습니다.

def findLanguages2(users: List[User],  
                   lookups: List[LanguageLookup]): LangState[List[Option[Language]]] =
  lookups.traverseS { lookup =>
    findLanguage(users, lookup.userName, lookup.repoName, lookup.langName).run
  }

traverseSstate 버전의 traverse 입니다. map 을 적용한 List[OptionT[LangState, Language]] 에 대해 LangState[List[Option[Language]] 를 돌려줍니다.

/** A version of `traverse` specialized for `State` */
final def traverseS[S, B](f: A => State[S, B]): State[S, F[B]] = F.traverseS[S, A, B](self)(f)  

State[S, A] 에 대해서

  • State[S, Option[List[A]] 를 얻고 싶다면 (all or nothing) traverseU
  • State[S, List[Option[A]] 를 얻고 싶다면 B = Option[A]List 로 감싸야 하므로 State[S, F[B]] 를 돌려주는 위해 traverseS 를 사용하면 됩니다.

EitherT

EitherTscalazEither 에 대한 모나드 트랜스포머입니다. 참고로, scalaz.Eitherscala.Either 과 달리 right-biased 입니다. Option 처럼요.

A \/ B is isomorphic to scala.Either[A, B], but \/ is right-biased, so methods such as map and flatMap apply only in the context of the "right" case.

scalaz.Either 에 대한 기본적인 설명은 Learning Scalaz - Either 에서 보실 수 있습니다.


EitherT 를 위한 간단한 모델을 만들어 보겠습니다.

  • 쿼리를 파싱하고, 실행하는 과정에서 상태QueryState 를 이용하고
  • 쿼리 파싱에 실패하면 수행하지 않고 종료하기 위해 scalaz.Either 를 사용합니다
// ref - https://speakerdeck.com/mpilquist/scalaz-state-monad

import scalaz._, Scalaz._

trait Model  
trait Query  
trait QueryResult

object QueryService {  
  def runQuery(s: String, model: Model): String \/ QueryResult = for {
    query <- parseQuery(s)
    result <- performQuery(query, model)
  } yield result

  def parseQuery(s: String): String \/ Query = "TODO".left
  def performQuery(q: Query, m: Model): String \/ QueryResult = "TODO".left
}

위 코드에 StateEitherT 를 추가하면

trait Model  
trait Query  
trait QueryResult  
trait Transaction 

object QueryService {  
  type TransactionState[A] = State[Transaction, A]
  type Transactional[A] = EitherT[TransactionState, String, A]

  def runQuery(s: String, model: Model): Transactional[QueryResult] = for {
    query <- EitherT(parseQuery(s).point[TransactionState])
    result <- EitherT(performQuery(query, model).point[TransactionState])
  } yield result

  def parseQuery(s: String): String \/ Query = ???
  def performQuery(q: Query, m: Model): String \/ QueryResult = ???
}

여기에 약간의 헬퍼 함수를 더하면,

def runQuery(s: String, model: Model): Transactional[QueryResult] = for {  
  query <- Transactional(parseQuery(s))
  result <- Transactional(performQuery(query, model))
} yield result

object Transactional {  
  import QueryService._
  def apply[A](e: String \/ A): Transactional[A] = liftE(e)
  def liftE[A](e: String \/ A): Transactional[A] = 
    EitherT(e.point[TransactionState])
}

이제 Transactional 이 이름 그대로의 역할을 할 수 있게 간단한 커넥션도 모델링 해 보겠습니다.

trait Transaction {  
  def closeConnection: Unit
  def commit: Unit = closeConnection
  def rollback: Unit = closeConnection
}

object QueryService {  
  type TransactionState[A] = State[Transaction, A]
  type EitherStringT[F[_], A] = EitherT[F, String, A]
  type Transactional[A] = EitherStringT[TransactionState, A]

  def parseQuery(s: String): String \/ Query =
    if (s.startsWith("SELECT")) s"Invalid Query: $s".left[Query]
    else (new Query {}).right[String]

  def performQuery(q: Query, m: Model): String \/ QueryResult =
    new QueryResult {}.right

  def runQuery(s: String, model: Model): Transactional[QueryResult] = for {
    query <- Transactional(parseQuery(s))
    result <- Transactional(performQuery(query, model))
    _ <- (modify { t: Transaction => t.commit; t }).liftM[EitherStringT]
  } yield result
}

여기서 EitherStringT 타입을 새로 만든건, liftM 을 사용하기 위해서입니다. 만약 liftM[EitherT] 를 이용해 리프팅을 하면, 다음과 같은 예외가 발생합니다.

Error:(37, 59) scalaz.EitherT takes three type parameters, expected: two  
    _ <- (modify { t: Transaction => t.commit; t }).liftM[EitherT]
                                                          ^

이제 parseQueryperformQuery 실패시 rollback 을 호출하는것을 구현하고, commit 을 헬퍼 함수로 변경하겠습니다.

def runQuery(s: String, model: Model): Transactional[QueryResult] = for {  
  query <- Transactional(parseQuery(s))
  result <- Transactional(performQuery(query, model))
  _ <- commit
} yield result

def commit: Transactional[Unit] =  
  (modify { t: Transaction => t.commit; t }).liftM[EitherStringT]

object Transactional {  
  import QueryService._
  def apply[A](e: String \/ A): Transactional[A] = e match {
    case -\/(error) =>
      /* logging error and... */
      liftTS(State[Transaction, String \/ A] { t => t.rollback; (t, e) })
    case \/-(a) => liftE(e)
  }

  def liftE[A](e: String \/ A): Transactional[A] =
    EitherT(e.point[TransactionState])

  def liftTS[A](tse: TransactionState[String \/ A]): Transactional[A] =
    EitherT(tse)
}

이제 다음처럼 실패시 롤백이 호출되고 for 자동으로 스탑되는것을 확인할 수 있습니다.

val t = new Transaction {}  
val model = new Model {}  
val result1 = runQuery("qqq", model).run.eval(t)  
println(result)

// output
parseQuery  
rollback  
-\/(Invalid Query: qqq)

val result2 = runQuery("SELECT", model).run.eval(t)  
println(result2)

// output
parseQuery  
performQuery  
\/-(QueryService$$anon$2@36804139)

만약 Transactioncommitted, rollbacked 등의 값을 추가하면 eval 대신 exec (run 도 가능) 으로 최종 상태인 Transaction 을 얻어 확인할 수 있습니다.

// https://github.com/scalaz/scalaz/blob/series/7.2.x/core/src/main/scala/scalaz/StateT.scala#L17

  /** An alias for `apply` */
  def run(initial: S1): F[(S2, A)] = apply(initial)

  /** Calls `run` using `Monoid[S].zero` as the initial state */
  def runZero[S <: S1](implicit S: Monoid[S]): F[(S2, A)] =
    run(S.zero)

  /** Run, discard the final state, and return the final value in the context of `F` */
  def eval(initial: S1)(implicit F: Functor[F]): F[A] =
    F.map(apply(initial))(_._2)

  /** Calls `eval` using `Monoid[S].zero` as the initial state */
  def evalZero[S <: S1](implicit F: Functor[F], S: Monoid[S]): F[A] =
    eval(S.zero)

  /** Run, discard the final value, and return the final state in the context of `F` */
  def exec(initial: S1)(implicit F: Functor[F]): F[S2] =
    F.map(apply(initial))(_._1)

  /** Calls `exec` using `Monoid[S].zero` as the initial state */
  def execZero[S <: S1](implicit F: Functor[F], S: Monoid[S]): F[S2] =
    exec(S.zero)

StateT

Easy Scalaz 1 - State 에서 언급했던 것 처럼

type State[S, A] = StateT[Id, S, A]  
type Id[+X] = X

// 더 엄밀히는,

type StateT[F[_], S, A] = IndexedStateT[F, S, S, A]  
type IndexedState[-S1, S2, A] = IndexedStateT[Id, S1, S2, A]  

StateT 에다가 혼합할 모나드 FId 를 준것이 State 입니다.

여기에 함수 replicateM 을 적용하면,

// https://speakerdeck.com/mpilquist/scalaz-state-monad
  "replicateM(10)" in {

    // def replicateM(n: Int): F[List[A]]
    val getAndIncrement: State[Int, Int] = State { s => (s + 1, s) }
    getAndIncrement.replicateM(10).run(0) shouldBe (10, (0 until 10).toList)
  }

따라서 StateF[_] 라 보면 이걸 F[List[_]] 로 만들어 주므로 여러개의 flatMap 이 중첩된 형태가 됩니다.

따라서 replicateM(100000) 등의 코드는 Stackoverflow 가 발생합니다.

이 문제를 해결하기 위해 Trampoline 을 이용할 수 있습니다.

Scalaz provides the Free data type, which when used with Function0, trade heap for stack

이럴때 Trampoline 을 사용하면, stackoverflow 를 피할 수 있습니다. (그만큼의 힙을 사용해서)

// type Trampoline[+A] = Free[Function0, A]

"replicateM(1000)" in {

  import scalaz.Free._

  val getAndIncrement: State[Int, Int] = State { s => (s + 1, s) }
  getAndIncrement.lift[Trampoline].replicateM(1000).run(0).run shouldBe (1000, (0 until 1000).toList)
}

Trampoline 은 후에 Free 를 살펴보면서 다시 보겠습니다.

References

]]>
<![CDATA[Easy Scalaz 1, State]]>

State

State 를 설명하는 수많은 문구들이 있지만, 타입만큼 간단한건 없습니다.

State[S, A] :: S => (S, A)  

A state transition, representing a function

S 를 받아 (S, A) 를 돌려주는 함수를, 타입클래스 State[S, A] 로 표현합니다.

더 엄밀히는, (scalaz 구현에서는) type State[S, A] = StateT[Id, S, A]

]]>
http://1ambda.github.io/easy-scalaz-1-state/f9bd7b24-522b-412b-8c4f-cd741c93bc28Mon, 12 Oct 2015 14:14:00 GMT

State

State 를 설명하는 수많은 문구들이 있지만, 타입만큼 간단한건 없습니다.

State[S, A] :: S => (S, A)  

A state transition, representing a function

S 를 받아 (S, A) 를 돌려주는 함수를, 타입클래스 State[S, A] 로 표현합니다.

더 엄밀히는, (scalaz 구현에서는) type State[S, A] = StateT[Id, S, A] where Id[+X] = X 인데 이것은 나중에 StateT 에서 다시 보겠습니다.

우선 기억해둘 것은 State함수 를 나타낸다는 사실입니다. 상태 S 를 변경하면서 A 를 만들어내는 함수를 말이지요. 즉, State 는 더도 말고 덜도 말고, 상태를 조작하는 함수 입니다. 여기에 모나드라고 하니, flatMap 같은 몇몇 함수가 추가된 것 뿐이지요.

State Basics

State 코드를 들춰보면, 아래와 같이 생겼습니다.

object State extends StateFunctions {  
  def apply[S, A](f: S => (S, A)): State[S, A] = new StateT[Id, S, A] {
    def apply(s: S) = f(s)
  }
}

trait StateFunctions extends IndexedStateFunctions {  
  def constantState[S, A](a: A, s: => S): State[S, A] = State((_: S) => (s, a))
  def state[S, A](a: A): State[S, A] = State((_ : S, a))
  def init[S]: State[S, S] = State(s => (s, s))
  def get[S]: State[S, S] = init
  def gets[S, T](f: S => T): State[S, T] = State(s => (s, f(s)))
  def put[S](s: S): State[S, Unit] = State(_ => (s, ()))
  def modify[S](f: S => S): State[S, Unit] = State(s => {
    val r = f(s);
    (r, ())
  })
}
  • State.apply 에 상태 S 를 조작하는 함수 f 를 먹이면 StateT 가 나오고
  • StateT.apply 에 초기 상태 S 를 먹이면 최종 결과물인 (S, A) 가 나옵니다

그리고 코드를 조금 만 더 따라가다 보면 applyaliasrun 이라는 함수가 제공되는걸 알 수 있습니다. (Scalaz StateT.scala #L10)

flatMap 으로 상태 조작함수 f 여러개를 엮다가 하다가 마지막에 run 으로 실행시킬것 같다는 느낌이 들죠?


이제 StateFunctions trait 로 제공되는 함수를 사용해 볼까요? 그냥 써보면 재미 없으니, Github 에서 각 Repository 마다 존재하는 star 를 가져오는 것을 간단히 모델링 해보겠습니다. 매번 네트워크 요청을 통해 가져오면 느리니까, Map[String, Int] 타입의 캐시도 포함시켜서요.

import scalaz._, Scalaz._ /* import all */

type Cache = Map[String, Int]

"create, run State" in {
  val s: State[Cache, Int] = State { c => (c, c.getOrElse("1ambda/scala", 0))}
  val c: Cache = Map("1ambda/scala" -> 1)

  // def run(s: S): (S, A)
  val (c1, star1) = s.run(c)
  val (c2, star2) = s.run(Map.empty)

  (c1, star1) shouldBe (Map("1ambda/scala" -> 1), 1)
  (c2, star2) shouldBe (Map(), 0)
}

이 작은 코드에서 우리가 다루는 상태는 Cache 입니다. 아직은 State { c => ... } 에서 받은 c: Cache 를 수정하지 않기 때문에 run 에서 돌려주는 상태 (State) 는 run 에 넘긴 것과 동일합니다. 그런고로 c == c1 == c2 입니다.

이번엔 상태를 변경하는 함수를 만들어 보겠습니다. 캐시에서 데이터를 가져오면, 캐시를 그대로 돌려주고 미스가 발생하면 캐시에 레포지토리 URL 을 추가하겠습니다.

def getStargazer(url: String): State[Cache, Int] = State { c =>  
  c.get(url) match {
    case Some(count) => (c, count)
    case None        => (c.updated(url, 0), 0)
  }
}

"getStargazer" in {
  val c: Cache = Map("1ambda/scala" -> 1)

  val s1 = getStargazer("1ambda/haskell")
  val (c1, star) = s1.run(c)

  (c1, star) shouldBe (c.updated("1ambda/haskell", 0), 0)
}

State 는 모나드기 때문에, for 내에서 이용할 수 있습니다. 아래에서 더 자세히 살펴보겠습니다.

State Monad, Applicative and Functor

모나드는 returnbind 를 가지고 특정한 규칙을 만족하는 타입 클래스를 말하는데요, scala 에서는 bindflatMap 이란 이름으로 제공되는 것 아시죠?

trait Monad[A] {  
  // sometimes called `unit`
  def return(a: A): M[A]
  def flatMap[B](f: A => M[B]): M[B]
}

scalaz 에선 Monad 는 아래의 두 타입클래스를 상속받아 구현됩니다.

  • Applicative.point (= return)
  • Bind.bind (= bind)
trait Bind[F[_]] extends Apply[F] { self =>  
  ...
  def bind[A, B](fa: F[A])(f: A => F[B]): F[B]
  ...
}

trait Applicative[F[_]] extends Apply[F] { self =>  
  ...
  def point[A](a: => A): F[A]
  ...
}

게다가 ApplyFunctor 를 상속받으므로

trait Apply[F[_]] extends Functor[F] { self =>  
  def ap[A,B](fa: => F[A])(f: => F[A => B]): F[B]
  ...

scalaz 에서 StateFunctor 이면서, Applicative 이고, Monad 입니다.

아래는 doobie 를 만든 @tpolecat 의 블로그에서 가져온 scalaz 타입 클래스 계층인데, 이 그림을 보면 왜 그런지 알 수 있습니다. (http://tpolecat.github.io/assets/scalaz.svg)

이제 State 가 모나드라는 사실을 알았으니, 위에서 작성했던 getStargazer 함수를 다시 작성해보겠습니다. for comprehension 을 사용할건데요,

  • 먼저 State[Cache, Int] 의 상태인 Cache 를 얻어와야 하므로 get 을 이용하고
  • 상태를 변경해야 하므로 modify 를 호출하겠습니다.
// State helper functions defined in `StateFunctions` trait
def state[S, A](a: A): State[S, A] = State((_ : S, a))  
def init[S]: State[S, S] = State(s => (s, s)) /* 상태 S 를 아웃풋 A 위치로 꺼냄 */  
def get[S]: State[S, S] = init  
def gets[S, T](f: S => T): State[S, T] = State(s => (s, f(s)))  
def put[S](s: S): State[S, Unit] = State(_ => (s, ()))  
def modify[S](f: S => S): State[S, Unit] = State(s => {  
  /* 상태 S 를 변경하는 함수를 받아, 적용하고 A 위치에 `()` 를 돌려줌 */
  val r = f(s);
  (r, ())
})

def getStargazer(url: String): State[Cache, Int] = State { c =>  
  c.get(url) match {
    case Some(count) => (c, count)
    case None        => (c.updated(url, 0), 0)
  }
}

def getStargazerWithFor(url: String): State[Cache, Int] =  
  for {
    c <- State.get[Cache]
    optCount = c.get(url)
    _ <- modify { c: Cache =>
      // same as `if (optCount.isDefined) c else c.updated(url, 0)`
      optCount match {
        case Some(count) => c
        case None        => c.updated(url, 0)
      }
    }
  } yield optCount.getOrElse(0)

When to use State

그러면, 언제 State 가 필요할까요? 하나의 상태 (State) 를 지속적으로 변경, 공유하면서 연산을 실행할 때 사용할 수 있습니다.

Building computations from sequences of operations that require a shared state.

예를 들어 HTTP 요청과 응답, 트랜잭션 등을 State 로 다루면서 연산을 조합해서 사용할 수 있습니다.

  • HttpRequest, HttpResponse, HttpSession
  • Database Transaction
  • Random Number Generator

Github Service Example

그러면 위에서 보았던 Cache 에 약간의 기능을 추가해 볼까요? 캐시 히트, 미스도 저장하고 캐시 히트는 최대 5분까지만 인정하기로 하지요. 오래된 캐시를 삭제하는 기능을 빼고 만들어 보면,

type URL = String  
type StarCount = Int

case class Timestamped(count: StarCount, time: DateTime)

case class Cache(hits: Int, misses: Int, map: Map[URL, Timestamped]) {  
  def get(url: URL): Option[Timestamped] = map.get(url)
  def update(url: URL, timestamp: Timestamped): Cache = {
    val m = map + (url -> timestamp)
    this.copy(map = m)
  }
}

object Cache {  
  def empty = Cache(0, 0, Map())
}

만약 State 가 없다면, 우리가 다루는 상태인 Cache 를 명시적으로 넘겨주고, 리턴받기 위해 이렇게 코드를 작성해야 할테지요. 여기서 c1 대신 c 를 쓰는 오타라도 발생한다면..

def stargazerCount(url: URL, c: Cache): (Cache, StarCount) = {  
  val (c1, optCount) = checkCache(url, c)

  optCount match {
    case Some(count) => (c1, count)
    case None => retrieve(url, c1)
  }
}

def checkCache(url: URL, c: Cache): (Cache, Option[StarCount]) =  
  c.get(url) match {
    case Some(Timestamped(count, time)) if !stale(time) =>
      (c.copy(hits = c.hits + 1), Some(count))
    case _ =>
      (c.copy(misses = c.misses + 1), None)
  }

def retrieve(url: URL, c: Cache): (Cache, StarCount) = {  
  val count = getStarCountFromWebService(url)
  val timestamp = Timestamped(count, DateTime.now)
  (c.update(url, timestamp), count)
}

def stale(then: DateTime): Boolean = DateTime.now > then + 5.minutes  
def getStarCountFromWebService(url: URL): StarCount = ...  


여기에 State 를 하나씩 적용해 보겠습니다.

def stargazerCount(url: URL, c: Cache): (Cache, StarCount) = {  
  val (c1, optCount) = checkCache(url, c)

  optCount match {
    case Some(count) => (c1, count)
    case None => retrieve(url, c1)
  }
}

먼저 State 타입을 적용하고, 그 후에 for 문을 적용한 뒤에, State.state 를 이용해서 조금 더 깔끔하게 바꾸면

// applying State 
def stargazerCount(url: URL): State[Cache, StarCount] =  
  checkCache(url) flatMap { optCount =>
    optCount match {
      case Some(count) => State { c => (c, count) }
      case None        => retrieve(url)
    }
  }

// use for-comprehension
def stargazerCount2(url: URL): State[Cache, StarCount] = for {  
  optCount <- checkCache(url)
  count <- optCount match {
    case Some(count) => State[Cache, StarCount] { c => (c, count) }
    case None        => retrieve(url)
  }
} yield count

// State.state
def stargazerCount(url: URL): State[Cache, StarCount] = for {  
  optCount <- checkCache(url)
  count <- optCount
    .map(State.state[Cache, StarCount])
    .getOrElse(retrieve(url))
} yield count

checkCache 함수에도 적용해 보겠습니다.

def checkCacheOrigin(url: URL, c: Cache): (Cache, Option[StarCount]) =  
  c.get(url) match {
    case Some(Timestamped(count, time)) if !stale(time) =>
      (c.copy(hits = c.hits + 1), Some(count))
    case _ =>
      (c.copy(misses = c.misses + 1), None)
  }

def checkCache1(url: URL): State[Cache, Option[StarCount]] = State { c =>  
  c.get(url) match {
    case Some(Timestamped(count, time)) if !stale(time) =>
      (c.copy(hits = c.hits + 1), Some(count))
    case _ =>
      (c.copy(misses = c.misses + 1), None)
  }
}

/**
 *  Has potential bug.
 *  Always use `State.gets` and `State.modify`.
 */
def checkCache2(url: URL): State[Cache, Option[StarCount]] = for {  
  c <- State.get[Cache]
  optCount <- State.state {
    c.get(url) collect { case Timestamped(count, time) if !stale(time) => count }
  }
  _ <- State.put(optCount ? c.copy(hits = c.hits + 1) | c.copy(misses = c.misses + 1))
} yield optCount

def checkCache(url: URL): State[Cache, Option[StarCount]] = for {  
  optCount <- State.gets { c: Cache =>
    c.get(url) collect { case Timestamped(count, time) if !stale(time) => count }
  }
  _ <- State.modify { c: Cache =>
    optCount ? c.copy(hits = c.hits + 1) | c.copy(misses = c.misses + 1)
  }
} yield optCount

checkCache2State.get State.put 때문에 버그가 발생할 수 있습니다. get 으로 꺼낸 뒤에 put 으로 넣으면, 이전에 어떤 상태가 있었든지, 덮어 씌우기 때문에 주의가 필요합니다. 일반적으로는 put 대신 modify 를 이용합니다.

def init[S]: State[S, S] = State(s => (s, s))  
def get[S]: State[S, S] = init  
def put[S](s: S): State[S, Unit] = State(_ => (s, ()))

def gets[S, T](f: S => T): State[S, T] = State(s => (s, f(s)))  
def modify[S](f: S => S): State[S, Unit] = State(s => {  

마지막으로 retrieve 함수도 수정해볼까요

def retrieveOrigin(url: URL, c: Cache): (Cache, StarCount) = {  
  val count = getStarCountFromWebService(url)
  val timestamp = Timestamped(count, DateTime.now)
  (c.update(url, timestamp), count)
}

def retrieve1(url: URL): State[Cache, StarCount] = State { c =>  
  val count = getStarCountFromWebService(url)
  val timestamp = Timestamped(count, DateTime.now)
  (c.update(url, timestamp), count)
}

def retrieve(url: URL): State[Cache, StarCount] = for {  
  count <- State.state { getStarCountFromWebService(url) }
  timestamp = Timestamped(count, DateTime.now)
  _ <- State.modify[Cache] { _.update(url, timestamp) }
} yield count

References

]]>
<![CDATA[Reactive Message Patterns w/ Actor Model, Chapter 1]]>

Why Enterprise Software Development Is Hard

엔터프라이즈 소프트웨어를 구현할 때 마주치는 문제점은, 고려해야할 것이 너무나 많다는 점입니다.

  • Physical Tiers
  • Application Servers
  • Software layers
  • Frameworks and Patterns
  • Toolkits
  • Databases
  • Messaging Systems
  • Third-Party Applications
  • ...

이런 요소들로 구성된 complexity stack 의 내부를 잘 살펴보면, 결국 관심사는 command 에 의해 생성된 domain event

]]>
http://1ambda.github.io/reactive-message-patterns-w-actor-model-chapter-1/5c6d3ec5-2b23-462a-94da-f5df4ae0bddaSun, 20 Sep 2015 16:33:22 GMT

Why Enterprise Software Development Is Hard

엔터프라이즈 소프트웨어를 구현할 때 마주치는 문제점은, 고려해야할 것이 너무나 많다는 점입니다.

  • Physical Tiers
  • Application Servers
  • Software layers
  • Frameworks and Patterns
  • Toolkits
  • Databases
  • Messaging Systems
  • Third-Party Applications
  • ...

이런 요소들로 구성된 complexity stack 의 내부를 잘 살펴보면, 결국 관심사는 command 에 의해 생성된 domain event 를 저장하는 일임을 알 수 있습니다.

Actor Model 은 여기에서 출발합니다. 불필요한 컴포넌트를 제외하고, commandevent 에만 집중할 수 있도록 추상화를 제공합니다.

  • What incoming messages (commands and/or events) do I accept?
  • What outgoing messages (commands and/or events) do I emit?
  • How can my state be mutated in reaction to incoming messages?
  • What is my supervision strategy for supervised actors?


Origin Of Actors

Actor Model 은 최근에 새롭게 만들어진 개념이 아니라, 1973년(Dr. Carl Hewitt) 부터 있었던 개념입니다. 다만 당시에는 컴퓨팅 파워가 부족했기 때문에 활용되지 않았을 뿐입니다. Actor Model 이 처음 만들어졌을 당시에는 CPU 클럭은 1MHz 남짓이었고 멀티코어 프로세서는 존재하지도 않았습니다.


Understanding Actors

Actor 는 하나의 컴퓨팅 객체로서 메시지를 받아 다음의 일들을 수행할 수 있습니다.

  • Send a finite number of messages to other actors
  • Create a finite number of new actors
  • Designate the behavior to be used for the next messages it receives

Actor System 에서는 모든것이 Actor 입니다. 따라서 Int, String 처럼 일종의 primitive type 으로 생각하면 더 이해가 쉽습니다.

Actor System 과 Actor 는 다음의 특성을 가지고 있습니다.

  • Communication via direct asynchronous message
  • State machines (FSM)
  • Share nothing
  • Lock-Free Concurrency
  • Parallelism

Akka 에서 추가적으로 제공하는 특성들은 다음과 같습니다.

  • Location Transparency
  • Supervision
  • Future / Promises


Concurrency and Parallelism

Concurrency describes multiple computation occurring simultaneously. Parallelism is concurrency but applied to achieving a single goal. Parallelism is achieved by dividing a single complex process into smaller tasks and executing them concurrently.

Amdahl’s law 에 의하면 병렬화해서 얻을 수 있는 최대 성능은, 병렬화 할 수 없는 부분에 의해서 제한됩니다.

따라서 시스템을 얼마나 병렬화 할 수 있도록 디자인하는가가 성능에 영향을 주게 됩니다. 이는 일반적으로 어려운 일이지만, Actor System 을 이용하면 atomic 연산 unit 인 Actor 를 기반으로 디자인할 수 있으므로 부가적인 계층(Tier) 보다는 로직(이 메시지를 받았을 때 어떤 일을 해야하는가)에 더 집중하게 되어 쉬운 일이 됩니다.

Akka 프로젝트 설명에서도 볼 수 있듯이, 분산 병렬 시스템을 만드는 것은 어려운 일이지만 대부분의 경우는 잘못된 추상(Abstaction), 도구(Tool) 을 이용하기 때문입니다. Actor Model 은 프로그래머가 더 쉬운 방법으로 분산 병렬 시스템을 디자인할 수 있도록 돕습니다.

We believe that writing correct concurrent & distributed, resilient and elastic applications is too hard. Most of the time it's because we are using the wrong tools and the wrong level of abstraction.

Akka is here to change that.

Using the Actor Model we raise the abstraction level and provide a better platform to build correct concurrent and scalable applications.


Non-determinism

Actor Model 이 비결정적이라는 비판들이 있습니다. 그러나 실제로 내부를 잘 살펴보면 Actor 그 자체는 deterministic atomic unit 입니다. 따라서 시스템을 Reactive 하게 구성하는 과정에서 프로그래머가 다루어야 하는 non-determinism 을 Actor Model 을 이용하면 더 간단하게 다룰 수 있습니다.


References

]]>
<![CDATA[Angular, Providers]]>

자그마한 프로젝트를 엇그제 시작했습니다. 오늘 해야 할 일은 Linkedin, Github API 를 붙이는 일인데, 그 전에 Angular 를 좀 보고 넘어가겠습니다. 아래는 angular-fullstack 으로 만들면 생성되는 템플릿 코드인데, 어디서 부터 시작해야할지 감이 안잡히네요!

angular.module('app', [  
  'ngCookies',
  'ngResource',
  'ngSanitize',
  'ui.router',
  'ui.bootstrap'
])
  .config(function ($stateProvider, $urlRouterProvider, $locationProvider, $httpProvider) {
    $urlRouterProvider
]]>
http://1ambda.github.io/angular-providers/fac918aa-4bc5-464c-ba73-2d2fad7e6a9dSat, 14 Mar 2015 16:42:33 GMT

자그마한 프로젝트를 엇그제 시작했습니다. 오늘 해야 할 일은 Linkedin, Github API 를 붙이는 일인데, 그 전에 Angular 를 좀 보고 넘어가겠습니다. 아래는 angular-fullstack 으로 만들면 생성되는 템플릿 코드인데, 어디서 부터 시작해야할지 감이 안잡히네요!

angular.module('app', [  
  'ngCookies',
  'ngResource',
  'ngSanitize',
  'ui.router',
  'ui.bootstrap'
])
  .config(function ($stateProvider, $urlRouterProvider, $locationProvider, $httpProvider) {
    $urlRouterProvider
      .otherwise('/');

    $locationProvider.html5Mode(true);
    $httpProvider.interceptors.push('authInterceptor');
  })

  .factory('authInterceptor', function ($rootScope, $q, $cookieStore, $location) {
    return {
      // Add authorization token to headers
      request: function (config) {
        config.headers = config.headers || {};
        if ($cookieStore.get('token')) {
          config.headers.Authorization = 'Bearer ' + $cookieStore.get('token');
        }
        return config;
      },

      // Intercept 401s and redirect you to login
      responseError: function(response) {
        if(response.status === 401) {
          $location.path('/login');
          // remove any stale tokens
          $cookieStore.remove('token');
          return $q.reject(response);
        }
        else {
          return $q.reject(response);
        }
      }
    };
  })

  .run(function ($rootScope, $location, Auth) {
    // Redirect to login if route requires auth and you're not logged in
    $rootScope.$on('$stateChangeStart', function (event, next) {
      Auth.isLoggedInAsync(function(loggedIn) {
        if (next.authenticate && !loggedIn) {
          $location.path('/login');
        }
      });
    });
  });

config, run

원문은 Angular Document: Module Loading & Dependencies

configurationrun blockbootstrap 과정에서 실행되는데

  • configuration block 에서는 provider, constantinjected 될 수 있고
  • run blockinjector 가 생성되고, 어플리케이션을 구동하기 위해 사용된 후에 실행됩니다. instanceconstantinjected 될 수 있습니다.
angular.module('myModule', []).

  config(function(injectable) { // provider-injector
    // you can only inject Providers (not instances)
    // into config block
  }).

  run(function(injectable) {    // instance-injector
    // you can only inject instances (not Providers)
    // into run blocks
  });

아래는 동일한 코드를 다른 메소드를 이용해 작성한 애플리케이션 초기화 코드입니다.

angular.module('myModule', []).  
  value('a', 123).
  factory('a', function() { return 123; }).
  directive('directiveName', ...).
  filter('filterName', ...);

// is same as

angular.module('myModule', []).  
  config(function($provide, $compileProvider, $filterProvider) {
    $provide.value('a', 123);
    $provide.factory('a', function() { retrun 123; });
    $compileProvider.directive('directiveName', ...);
    $filterProvider.register('filterName', ...);
  });  

배운것보다 모르는게 더 많이 생겼습니다. Provider, $provide, injectable 이 뭘까요?


Providers

원문은 Angular Document: Providers

angular app 에서 쓰이는 오브젝트들은 intector service 에 의해서 인스턴스화(instantiated) 됩니다. injector 는 두 타입의 오브젝트를 만드는데,

(1) Services: are objects whose API is defined by the developer writing the service
(2) Specialized objects: conform to a specific angular framework API. These objects are one of controllers, directives, filters or animations

injector 가 이러한 서비스를 만들기 위해서는 recipe 를 알려줘야 하는데, 크게 5가지 recipe 가 있습니다.

가장 유명한건 Provider 입니다. 그 외에 Provider 를 이용해 만든 Value, Factory, Service, Constant 가 있습니다.

angular module 은 하나 이상의 Provider 를 포함할 수 있습니다. 애플리케이션이 시작될때 Angularinjector 의 새로운 인스턴스를 만들고, ng 모듈, 애플리케이션 모듈, 그리고 그 dependencies 에 있는 모든 recipe 를 하나의 레지스트리에 등록합니다. 그리고 이후에 필요할때마다 injector 는 이 레지스트리에 새로운 인스턴스를 만들어야 할지, 아니면 존재하는 것을 사용할지 질의합니다.

Value recipe 를 이용한 간단한 예제 를 보겠습니다.

var myApp = angular.module('myApp', []).  
              value('clientId', 'a12345654321x');

myApp.controller('myController', ['clientId',  
                                  function(clientId) {
  this.clientId = clientId;
}]);                              

myApp 모듈에 정의되어 있는 clientId Value recipe 를 등록하고 컨트롤러에서 사용했습니다.


Factory

myApp.factory('apiToken', ['clientId', function apiTokenFactory(clientId) {

  var encrypt = function(data1, data2) {
  // encryption algorithm:
    return (data + ':' + data2).toUpperCase();
  };

  var secret = window.localStorage.getItem('myApp.secret');
  var apiToken = encrypt(clientId, secret);

  return apiToken;
}]);

Factory recipe 를 이용해서 apiToken 서비스를 정의했습니다. 이 서비스는 Value recipe 를 이용해 만든 clientId 서비스에 의존합니다.


Service

apiToken 서비스를 이용하는 다른 서비스를 Service recipe 를 이용해서 만들어 볼텐데, 동시에 Service recipe 가 어떤 역할을 하는지 비교하기 위해 Factory recipe 로도 만들어 보겠습니다.

function UnicornLauncher(apiToken) {

  this.launchedCount = 0;
  this.launch = function() {
    // Make a request to the remote API and include the apiToken
    ...
    this.launchedCount++;
  }
}

myApp.factory('unicornLauncher', ["apiToken", function(apiToken) {  
  return new UnicornLauncher(apiToken);
}]);

// is same as
myApp.service('unicornLauncher', ["apiToken", UnicornLauncher]);  

Factory recipe 로도 만들 수 있지만, 일반적으로 Service recipenew 와 함께 호출되는 서비스를 정의하기 위해 사용합니다. Stackoverflow: Factory vs Service 에서도 그 답변을 찾을 수 있습니다.

예를 들어서 위에서 우리가 정의한 unicornLauncher 서비스는, UnicornLauncher 생성자를 new 로 호출됩니다.


아래는 대략적인 두 함수의 구성입니다.

function factory(name, factoryFn) {  
    return provider(name, { $get: factoryFn }); 
}

function service(name, constructor) {  
    return factory(name, ['$injector', function($injector) {
      return $injector.instantiate(constructor);
    }]);
}

$injectorprovider 에 의해 정의된 인스턴스를 angular app 내에서 조회하고, 생성할 수 있습니다. 이외에도 메소드를 호출하거나, 모듈을 로드할 수 있습니다.


Provider

Provider recipeServiceFactory 등 다른 recipe 를 구성하는 코어 컴포넌트입니다. 문법적으로는 $get 을 구현한 커스텀 타입입니다. 이 $get 메소드는 Factory recipe 에서 사용했던 것과 같은 factory function 입니다.

다시 말해서, Factory recipe 만들때 하는 일은 empty Provider$get 을 이용해 정의된 factory function 을 가져오는 일입니다.

Provider recipe 는 반드시 애플리케이션이 시작 되기 전에 생성되야 하는 application-wide configuration 을 위한 API 를 정의할때만 사용해야 합니다.

myApp.provider('unicornLauncher', funtion UnicornLauncherProvider() {  
  var useTinfoilShielding = false;

  this.useTinfoilShielding = function(vaue) {
    useTinfoilShielding = !!value;
  };

  this.$get = ["apiToken", function unicornLauncherFactory(apiToken) {
    return new UnicornLauncher(apiToken, useTinfoilShielding);
  }];
});

myApp.config(["unicornLauncherProvider", function(unicornLauncherProvider) {  
  unicornLauncherProvider.useTinfoilShielding(true);
}]);

처음에 configuration block config 를 언급하면서 provider, constantinjected 될 수 있다고 말했었는데, 이런 이유에서입니다.

regular instance injector 와는 달리 provider injector 에 의해 실행되는 이런 injection 을 통해 모든 provider 가 인스턴스화 (instantiated) 됩니다.

angular 애플리케이션이 부트스트랩되는 동안, provider 가 구성되고, 생성되는 동안에는 service 에 접근할 수 없습니다. 이는 service 가 아직 생성되지 않았기 때문입니다.

configuration phase 가 지난 후에야 services 가 생성되고, 이 단계를 run phase 라 부릅니다. 이 때문에 run block 에서 instanceconstantinjected 될 수 있다고 위에서 언급한 것입니다.


Special Purpose Objects

앞서 Angular 에서 쓰이는 모든 오브젝트는 intector service $injector 에 의해서 초기화 된다고 했었습니다. 일반적인 서비스 오브젝트와, 특별한 목적을 가진 오브젝트들이 있다고 언급하기도 했지요.

이런 특별한 오브젝트들은 프레임워크를 확장하는 플러그인으로서 Angular 에서 정의한 interface 를 구현해야 하는데, 이 인터페이스는 Controller, Directive, Filter, Animation 입니다.

Controller 오브젝트를 제외하고는 이러한 special object 를 생성하기 위해 injectorFactory recipe 를 이용합니다. 따라서 인자로 넣어준 팩토리 함수가 디렉티브를 만들기 위해 호출됩니다.

myApp.directive('myPlanet', ['planetName', function myPlanetDirectiveFactory(planetName) {  
  // directive definition object
  return {
    restrict: 'E',
    scope: {},
    link: function($scope, $element) { $element.text('Planet: ' + planetName); }
  }
}]);


Controller

myApp.controller('DemoController', ['clientId', function DemoController(clientId) {  
  this.clientId = clientId;
}]);

Controller 는 조금 다르게, Factory recipe 를 이용하지 않습니다. 인자로 정의한 constructor function 함수가 모듈과 함께 등록됩니다.

애플리케이션이 DemoController 가 필요할때마다 매번 constructor 를 통해서 인스턴스화(instantiated) 합니다. 일반적인 service 와는 다르게, 컨트롤러는 싱글턴이 아닙니다.

지금까지 배운 내용을 정리하면

  • The injector uses recipes to create two type of objects: services and special purpose objects
  • There are five recipe types that define how to create objects: Value, Factory, Service, Provide, and Constant
  • Factory and Service are the most commonly used recipes. The only differences between them is that the Service recipe works better for objects of a custom type, while the Factory can produce primitives and functions
  • The Provider recipe is the core recipe type and all the other ones are just syntactic sugar on it
  • Provider is the most complex recipe type. You don't need it unless you are building a reusable piece of code that needs global configuration
  • All special purpose objects except for the Controller are defined via Factory recipes


Dependency Injection

service$injector 에 의해서 싱글턴 인스턴스가 만들어지고, $injector.get() 을 통해 얻을 수 있습니다. 만약 캐시된 인스턴스가 있다면 가져오고 없으면 새로 만듭니다. 아래는 외부에서 injector 를 통해 내부 서비스를 접근하는 방법입니다.

var injector = angular.injector(['myModule', 'ng']);  
var greeter = injector.get('greeter');  


Refs

(1) [http://galleryhip.com/angular-js-icon.html)
(2) Angular Document
(3) Webdeveasy: AngularJS Q
(4) Webdeveasy: AngularJS Interceptor

]]>
<![CDATA[Coding The Matrix 3]]>

Null space of a matrix is a vector space

standard geneartor 를 이용해서 f(x) = M * x 에서의 M 의 컬럼을 알아낼 수 있다.

어떤 함수 fM * x 형태로 정의되면, flinear function 이다.

어떤 함수 fkernelimage0 으로 하는 집합이다. 다시

]]>
http://1ambda.github.io/coding-the-matrix-3/715b3e8b-1821-4204-98cf-48f2a029e11bSat, 14 Mar 2015 16:41:34 GMT

Null space of a matrix is a vector space

standard geneartor 를 이용해서 f(x) = M * x 에서의 M 의 컬럼을 알아낼 수 있다.

어떤 함수 fM * x 형태로 정의되면, flinear function 이다.

어떤 함수 fkernelimage0 으로 하는 집합이다. 다시 말해서 f(x) = M * x 에 대해 null matrix xkernel

linear function f is one-to-one iff its kernel is a trivial vector space

위에 나온 속성은 상당히 중요하다. 왜냐하면 trivial kernel 이면, 다시 말해서 null matrixtrivial 이면, fimage b 는 아무리 많아봐야 하나이기 때문이다.

imageentire co-domain 과 같으면 onto 다.


matrix-vector functioncomposition 은 위처럼 쉽게 증명 가능하다. AB * x

이걸 이용하면 matrix-matrix multiplicationassociativity 도 쉽게 증명 가능하다. (AB)C = A(BC)


Invertible

두 함수가 inverse 관계면 두 매트릭스도 inverse 관계다. 그리고 한 매트릭스의 inverse matrix 가 존재하면 invertible 또는 singular 라 부르며, 아무리 많아봐야 하나의 inverse 만 가진다.


invertible matrix 가 중요한 이유는, invertible matrix 가 존재하면 finvertible 이고, 그 말은 fone-to-one, onto 라는 소리다. 따라서 f(u) = b 에 대해 적어도 하나의 솔루션이 존재하고 (onto), 아무리 많아봐야 하나의 솔루션이 존재한다는 뜻이다 (one-to-one)


함수처럼 매트릭스도 A, Binvertible 일때만 AB 도 그러하다.


AB 에 대해 A, B 가 서로의 inverseABidentity matrix 지만 그 역은 성립하지 않는다.

위 그림의 A 에서 볼 수 있듯이 null spacetrivial 하지 않기 때문에 one to one 이 아니어서 Ainvertible 이 아니다.

AB, BA 가 모두 identity matrix 여야 A, B 가 서로 inverse 다.

매트릭스 Mone-to-one 인지는 trivial kernel 인지를 판별하면 된다. f(x) = M * xlinear function 이기 때문에 trivial kernel 이면 Mone-to-one 이다.

onto 인지는 어떻게 알 수 있을까?


Summary

지금 까지의 내용을 정리하면

  1. u1a * x = b 의 솔루션일때, Va * x = 0 의 솔루션 셋이라 하면, u1 + Va * x = b 의 솔루션 셋이다. 다시 말해서 Vnull matrix
  2. f(x)M * x 형태로 나타낼 수 있으면 linear function 이다.
  3. trivial kernel 이면 linear function fone-to-one 이고, linear function fone-to-one 이면 trivial kernel 을 가진다.


Refs

(1) Title image
(2) Coding the Matrix by Philip Klein

]]>
<![CDATA[Cloud Computing, Paxos]]>

대부분의 분산 서버 벤더들은 99.99999%reliability 를 보장하지만, 100%는 아닙니다. 왜그럴까요? 그들이 못해서가 아니라 consensus 문제 때문입니다.

The fault lies in the impossibility of consensus

Consensus 문제가 중요한 이유는, 많은 분산 시스템이 consensus 문제이기 때문입니다.

  • Perfect Failure Detection
  • Leader Election
  • Agreement (harder than consensus)


일반적으로 서버가 많으면

]]>
http://1ambda.github.io/cloud-computing-paxos/fa427789-33eb-4728-913a-ab4b1cfbf14eSat, 07 Mar 2015 19:44:32 GMT

대부분의 분산 서버 벤더들은 99.99999%reliability 를 보장하지만, 100%는 아닙니다. 왜그럴까요? 그들이 못해서가 아니라 consensus 문제 때문입니다.

The fault lies in the impossibility of consensus

Consensus 문제가 중요한 이유는, 많은 분산 시스템이 consensus 문제이기 때문입니다.

  • Perfect Failure Detection
  • Leader Election
  • Agreement (harder than consensus)


일반적으로 서버가 많으면 다음의 일들을 해야합니다.

  • Reliable Multicast: Make sure that all of them receive the same updates in the same order as each other
  • Membership/Failure Detection: To keep their own local lists where they know about each other, and when anyone leaves or fails, everyone is updated simultaneously
  • Leader Election: Elect a leader among them, and let everyone in the group know about it
  • Mutual Exclusion: To ensure mutually exclusive access to a critical resource like a file

이 문제들은 대부분 consensus 와 연관되어 있습니다. 더 직접적으로 연관되어 있는 문제들은

  • The ordering of messages
  • The up/down status of a suspected failed process
  • Who the leader is
  • Who has access to the critical resource


Consensus Problem

모든 프로세스(노드, 서버)가 같은 value 를 만들도록 해야 하는데, 몇 가지 제약조건이 있습니다.

  • validity: if everyone propose same value, then that's what's decided
  • integrity: decided value must have been proposed by some process
  • non-triviality: there is at least one initial system state that leads to each of the all-0's or all-1's outcomes

non-triviality 는 쉽게 말해서, 모두 0 이거나 모두 1 일 수 있는 상태가 있어야 한다는 뜻입니다. 왜냐하면 항상 0 이거나 1 만 나오면 trivial 하기 때문입니다. 별 의미가 없죠.


Models

consensus 문제는 분산 시스템 모델에 따라 달라집니다. 모델은 크게 2가지로 나눌 수 있는데

(1) Synchronous Distributed System Model

  • Each message is received within bounded time
  • Drift of each process' local clock has a known bound
  • Each step in a process takes lb < time < ub

동기 시스템 모델에서는 consensus 문제를 풀 수 있습니다.

(2) Asynchronous Distributed System Model

  • Nobounds on process execution
  • The drift rate of a clock is arbitrary
  • No bounds on message transmission delay

일반적으로 비동기 분산 시스템 모델이 더 일반적입니다, 그리고 더 어렵죠. 비동기를 위한 프로토콜은 동기 모델 위에서 작동할 수도 있으나, 그 역은 잘 성립하지 않습니다.

비동기 분산 시스템 모델에서는 consensus 문제는 풀 수 없습니다

  • Whatever protocol/algorithm you suggest, there is always a worst-case possible execution with failures and message delays that prevens the system from reaching consensus
  • Powerful result(see the FLP proof)
  • Subsequently, safe and probabilistic solution have become popular (e.g Paxos)


Paxos in Syncronous Systems

동기 시스템이라 가정합니다. 따라서

  • bounds on message dealy
  • bounds on upper bound on clock drift rates
  • bounds on max time for each process step
  • processes can fail by stopping

  • 아무리 많아야 f 개의 프로세서에서 crash 가 나고
  • 모든 프로세서는 round 단위로 동기화 되고, 동작하며
  • reliable communication 을 통해 서로 통신합니다

value_i^rround r 의 시작에 P_i 에게 알려진 value 의 집합이라 라 하겠습니다.

f+1 라운드 후에 모든 correct 프로세스는 같은 값의 집합을 가지게 되는데, 귀류법으로 쉽게 증명할 수 있습니다.


비동기 환경에서는, 아주아주아주아주아주 느린 프로세서와 failed 프로세서를 구분할 수 없기 때문에, 나머지 프로세서들이 이것을 결정하기 위해 영원히 기다려야 할지도 모릅니다. 이것이 기본적인 FLP Proof 의 아이디어입니다. 그렇다면, consensus 문제를 정말 풀기는 불가능한걸까요?

풀 수 있습니다. 널리 알려진 consensus-solving 알고리즘이 있습니다. 실제로는 불가능한 consensus 문제를 풀려는 것이 아니라, safetyeventual liveness 를 제공합니다. 야후의 zookeeper 나 구글의 chubby 등이 이 알고리즘을 이용합니다.

safety 는 서로 다른 두개의 프로세서가 다른 값을 제출하지 않는것을 보장하고, (No two non-faulty processes decide different values) eventual liveness 는 운이 좋다면 언젠가는 합의에 도달한다는 것을 말합니다. 근데 실제로는 꽤 빨리 consensus 문제를 풀 수 있습니다.

본래는 최적화때문에 더 복잡한데, 위 슬라이드에서는 간략화된 paxos 가 나와있습니다. paxosround 마다 고유한 ballot id 가 할당되고, 각 round 는 크게 3개의 비동기적인 phase 로 분류할 수 있습니다.

  • election: a leader is elected
  • bill: leader proposes a value, processes ack
  • law: leader multicasts final value

먼저 potential leaderunique ballot id 를 고르고, 다른 프로세서들에게 보냅니다. 다른 프로세스들의 반응에 의해서 선출될 수도 있고, 선출되지 않으면 새로운 라운드를 시작합니다.

  • Because becoming a leader requires a majority of votes, and any two majorities intersect in at least one process, and each process can only vote once.

리더가 다른 프로세스들에게 v 를 제안하고, 프로세스들은 지난 라운드에 v' 를 결정했었으면 v=v' 를 이용해 값을 결정합니다.

만약 리더가 majority 의 긍정적인 반응을 얻으면 모두에게 그 결정을 알리고 각 프로세서는 합의된 내용을 전달받고, 로그에 기록하게 됩니다.

사실 이 과정은 응답을 리더가 받는 단계에서 결정되는 것이 아니라, 프로세서들이 proposed value 를 듣는순간 결정됩니다. 따라서 리더에서 failure 가 일어나도, 이전에 결정되었던 v' 을 이용할 수 있습니다.


이전에도 언급했듯이 safety 는 두개의 서로 다른 프로세서의 의해서 다른 값이 선택되지 않음을 보장합니다. 이는 잠재적 리더가 있다 하더라도 현재 리더와, 잠재적 리더에게 응답하는 majority (반수 이상) 을 교차하면 적어도 하나는 v' 를 응답하기 때문에 bill phase 에서 정의한대로 이전 결과인 v' 가 사용됩니다.

그림에서 볼 수 있듯이 영원히 끝나지 않을수도 있지만, 실제로는 꽤 빠른시간 내에 합의에 도달합니다. (eventualy-live in async systems)


Refs

(1) Title Image
(2) Cloud Computing Concept 1 by Indranil Gupta, Coursera

]]>
<![CDATA[Cloud Computing, Multicast]]>

multicast 는 클라우드 시스템에서 많이 사용됩니다. Cassandra 같은 분산 스토리지에서는 write/read 메세지를 replica gorup 으로 보내기도 하고, membership 을 관리하기 위해서 사용하기도 합니다

그런데, 이 multicastordering 에 따라서 correctness 에 영향을 줄 수 있기 때문에 매우 중요합니다. 자주 쓰이는 기법으로 FIFO, Casual, Total 이 있는데 하나씩 살펴보겠습니다.

]]>
http://1ambda.github.io/cloud-computing-multicast/1aa1fadb-da2f-4163-af61-a68ed5cef2acSat, 07 Mar 2015 17:20:18 GMT

multicast 는 클라우드 시스템에서 많이 사용됩니다. Cassandra 같은 분산 스토리지에서는 write/read 메세지를 replica gorup 으로 보내기도 하고, membership 을 관리하기 위해서 사용하기도 합니다

그런데, 이 multicastordering 에 따라서 correctness 에 영향을 줄 수 있기 때문에 매우 중요합니다. 자주 쓰이는 기법으로 FIFO, Casual, Total 이 있는데 하나씩 살펴보겠습니다.


Ordering

FIFO 를 이용한다면, 보낸 순서대로 도착하게 됩니다.

casual ordering 에서는 반드시 casuality-obeying order 로 전달해야 합니다. 예를 들어 위 그림에서는 M1:1 -> M3:1 이기 때문에 반드시 그 순서대로 받아야 합니다. concurrent event 는 어떤 순서로 받아도 상관 없습니다.


casual ordering 이면 FIFO ordering 입니다. 왜냐하면 같은 프로세스에서 보낸 casuality 를 따르면 그게 바로 FIFO 이기 때문입니다. 역은 성립하지 않습니다.

일반적으로는 casual ordering 을 사용합니다. 서로 다른 친구로부터 댓글이 달렸는데, 늦게 달린 친구의 댓글이 먼저 보인다면 당연히 말이 되지 않습니다.

total orderingatomic broadcast 라 부르는데, 모든 프로세스가 같은 순서로 메시지를 받는것을 보장합니다.

  • Since FIFO/Casual are orthogonal to Total, can have hybrid ordering protocol too (e.g FIFO-total, Casual-total


FIFO Ordering Impl

  • 각 프로세스는 seq number 로 구성된 벡터를 유지하고,
  • 프로세스에서 메시지를 보낼때 마다 자신의 seq number 를 하나 증가 시켜서 보냅니다
  • 메시지를 받았을때, 자신의 벡터 내에 있는 값 + 1 일 경우에만 벡터 값을 +1 한뒤 전달하고, 아니면 +1 인 값이 올 때까지 버퍼에 넣고 기다립니다

예제를 보면


Total Ordering Impl

sequencer-based approach 입니다. 먼저 하나의 프로세스가 sequencer 로 선출된 뒤, 어떤 프로세스가 메세지를 보낼때마다 그룹 뿐만 아니라 sequencer 에게 보내게 됩니다.

sequencer 는 글로벌 시퀀스 S 를 유지하면서, 메시지 M 을 받을때마다 S++ 해서 <M, S> 로 멀티캐스트를 보냅니다.

각 프로세스에서는 local 에 글로벌 시퀀스 Si 를 유지합니다. 만약 프로세스가 메세지를 받는다면 Si + 1 = S(M) 값을 글로벌 시퀀서로부터 받을때까지 기다리고, 받은 후에야 Si++ 하고 전달합니다.


Casual Ordering Impl

자료구조 자체는 같으나, casuality 를 검사하기 위해 sendervector 전체를 보냅니다. receiver 는 메세지를 받으면 다음 두 조건을 만족하기 전까지 버퍼에 넣습니다

  • M[j] = P_i[j] + 1
  • M[k] <= P_i[k], (k != j)

두번째 조건을 해석하면, 자신의 벡터도 다음 프로세스에게 전달해야 하기 때문에 M[k] 이후의 벡터만 가지고 있어야 전달할 수 있다는 뜻입니다. (M[j] 는 제외)

이 두 조건이 만족되야만 P_i[j] = M[j] 로 세팅하고 M 을 전달합니다.


Reliable Multicast

reliable 이란, 루즈하게 말하자면 모든 receiver 가 메세지를 받는다는 뜻입니다. ordering 과는 orthogonal 하기 때문에 Reliable-FIFO, 등등 구현이 가능합니다. 더 엄밀한 정의는

  • need all correct (non-faulty) processes to receive the same set of multicasts as all other correct processes

단순히 reliable unicast 를 여러개 보내는것 만으로는 부족합니다. 왜냐하면 sender 에서 failure 가 일어날 수 있기 때문입니다

비효율적이지만, reliable 합니다.


Virtual Synchrony

virtual sinchrony 혹은 view synchrony 라 불리는데, 이것은 failure 에도 불구하고 multicast orderingreliability 를 얻기 위해 membership protocolmulticast protocol 과 같이 사용합니다.

각 프로세스가 관리하는 membership listview 라 부릅니다. virtual synchrony 프로토콜은 이런 view changecorrect process 에 올바른 순서대로 전달됨을 보장합니다.

Virtual Synchrony 프로토콜은 다음을 보장합니다.

  • the set of multicasts delivered in a given view is the same set at all correct processes that were in that view
  • the sender of the multicast message also belongs to that view
  • if a process P_i doesn't not deliver a multicast M in view V while other processes in the view V delivered M in V, then P_i will be forcibly removed from the next view delivered after V at the other processes

다시 말해서, multicast 메세지는 같이 전달된 view 내에 있던 다른 프로세스에서 모두 동일합니다. 그리고 view V 내에 있는 어떤 프로세스가 M 을 전달하지 못할 경우, 다른 프로세스의 next view 에서 제거됩니다.


  • Called "virtual synchrony" since in spite of running on an asynchronous network, it gives the appearance of a synchronous network underneath that obeys the same ordering at all processes

그러나 consensus 를 구현하는데는 쓸 수 없습니다. partitioning 에 취약하기 때문입니다.

정리하자면 multicast 는 클라우드 시스템에서 중요한 요소입니다. 필요에 따라서 ordering, reliability, virtual synchorny 를 구현할 수 있습니다.


Refs

(1) Title Image
(2) Cloud Computing Concept 1 by Indranil Gupta, Coursera

]]>
<![CDATA[Cloud Computing, Snapshots]]>

이번시간에는 Distributed Snapshot 에 대해서 배웁니다. 클라우드 환경에서 각 어플리케이션(혹은 서비스) 는 여러개의 서버 위에서 돌아갑니다. 각 서버는 concurrent events 를 다루며, 서로 상호작용합니다. 이런 환경에서 global snapshot 을 캡쳐할 수 있다면

  • check pointing: can restart distributed application on failure
  • garbage collection of objects: object at servers that don't
]]>
http://1ambda.github.io/cloud-computing-snapshot/7d51c274-e6e6-41dd-a37e-b548da6e711cSat, 07 Mar 2015 13:57:56 GMT

이번시간에는 Distributed Snapshot 에 대해서 배웁니다. 클라우드 환경에서 각 어플리케이션(혹은 서비스) 는 여러개의 서버 위에서 돌아갑니다. 각 서버는 concurrent events 를 다루며, 서로 상호작용합니다. 이런 환경에서 global snapshot 을 캡쳐할 수 있다면

  • check pointing: can restart distributed application on failure
  • garbage collection of objects: object at servers that don't have any other objects(ay any servers) with pointers to them
  • deadlock detection: useful in database transaction systems
  • termination of computation: useful in batch computing systems like Folding@Homes, SETI@Home

global snapshot 은 두 가지를 포함합니다.

(1) Individual state of each process (2) Individual state of each communication channel

global snapshot 을 만드는 한가지 방법은 모든 프로세스의 clock 을 동기화 하는 것입니다. 그래서 모든 프로세스에게 time t 에서의 자신의 상태를 기록하도록 요구할 수 있습니다. 그러나

  • Time synchorization always has error
  • Doesn't not record the state of meesages in the channels

지난 시간에 보았듯이, synchronization 이 아니라 casuality 로도 충분합니다. 프로세스가 명령을 실행하거나, 메시지를 받거나, 메시지를 보낼때마다 global system 가 변합니다. 이를 저장하기 위해서 casuality 를 기록하는 방법을 알아보겠습니다.


Chandy-Lamport Algorithm

시작 전에 system model 을 정의하면

  • N Processes in the system
  • There are two uni-directional communication channels between each ordered process pair P_j -> P_i, P_i -> P_j
  • communication channels are FIFO ordered
  • No failure
  • All messages arribe intact, and are not duplicated

requirements

  • snapshot 때문에 application 의 작업에 방해가 일어나서는 안됩니다
  • 각 프로세스는 자신의 state 를 저장할 수 있어야 합니다
  • global state 는 분산회되어 저장됩니다 (collected in a distributed manner)
  • 어떤 프로세스든지, snapshot 작업을 시작할 수 있습니다

  • 프로세스 P_imarket 메세지를 만들고, 자신을 제외한 다른 N-1 개의 프로세스에게 보냅니다
  • 동시에 P_iincoming channel 을 레코딩하기 시작합니다

(1) 만약 P_imarker 메시지를 처음 받는다면

  • 만약메시지를 받은 프로세스 P_i 에서는 자신의 state 를 기록하고
  • 자신을 제외한 프로세스들에게 marker 보내고
  • incoming channel 을 레코딩하기 시작합니다

(2) P_i 가 이미 market 메세지를 받은적이 있다면

  • 이미 해당 채널의 모든 메세지를 기록중이었으므로, 레코딩을 끝냅니다

이 알고리즘은 모든 프로세스가 자신의 state 와 모든 channel 을 저장하면 종료됩니다.


Consistent Cuts

Chandy-Lamport 알고리즘은 casuality 를 보장합니다. 이에 대해 증명하기 전에 먼저, consistent cut 이란 개념을 보고 가겠습니다.

  • Cut: time frontier at each process and at each channel. Events at the process/channel that happen before the cut are in the cut and happening after the cut are out of the cut

  • Consistent Cut: a cut that obeys casuality. A cut C is a consistent cut iff for each pair of event e f in the system, such that event e is in the cur C and if f -> e

다시 말해서 eC 내에 있고, f -> e 라면 fC 에 있어야만 consistent cut 이란 뜻입니다.

Fcut 내에 있지만, 올바르게 캡쳐되어 메시지 큐 내에서 전송중임을 snapshot 에서 보장합니다. 하지만 G -> D 같은 경우는, Dcut 내에 있지만 G 가 그렇지 않아 inconsistent cut 입니다.

Chandy-Lamport Global Snapshot 알고리즘은 항상 consistent cut 을 만듭니다. 왜 그런가 증명을 보면

ei -> ej 를 보장한다는 말은 스냅샷 안에 두 이벤트가 있다는 뜻입니다. 따라서 ej -> <P_j records its state> 일때 당연히 ei -> <P_i records its state> 와 같은 말입니다.

만약 ej -> <P_j records its state> 일때 <P_i records its state> -> ei 라 합시다.

그러면 ei -> ej 로 가는 regular app message 경로를 생각해 봤을때, P_i 가 먼저 자신의 상태를 기록하기 시작했으므로 marker 메세지가 먼저 날라갈겁니다. (FIFO) 그러면 위에서 말한 ei -> ej 경로를 타고 marker 메세지가 먼저 가게되고 P_j 는 자신의 상태를 먼저 기록하게 됩니다. 따라서 P_j 에서 ej 보다 자신의 상태를 기록하는 것이 먼저이므로 ejout of cut 이고, 모순입니다.


Safety and Liveness

분산시스템의 correctness 와 관련해서 safetyliveness 란 개념이 있습니다. 이 둘은 주로 혼동되어 사용되는데, 둘을 구별하는 것은 매우 중요합니다.

  • distributed computation will terminate eventually
  • every failure is eventually deteced by some non-faulty process

  • there is no deadlock in a distributed transaction system
  • no object is orphaned
  • accuracy in failure detector
  • no two processes decide on different values

failure detectorconcensus 의 경우에서 볼 수 있듯이 completenessaccuracy 두 가지를 모두 충족하긴 힘듭니다.


global snapshot 은 한 상태 S 이고, 여기서 다른 스냅샷으로의 이동은 casual step 을 따라 이동하는 것입니다. 따라서 liveness 와, safety 와 관련해 다음과 같은 특징이 있습니다.

Chandy-Lamport 알고리즘은 stable 한지를 검사하기 위해 사용할 수도 있습니다. 여기서 stable 하다는 것은, 한번 참이면 그 이후에는 계속 참인 것을 말합니다. 이는 알고리즘이 casual correctness 를 가지기 때문입니다.


Refs

(1) Title Image
(2) Cloud Computing Concept 1 by Indranil Gupta, Coursera

]]>
<![CDATA[Coding The Matrix 2, Vector Space]]>

Linear Combinations

bv1, ..., vn 이 주어졌을때

  • a1, ..., an 을 찾을 수 있을까요?
  • 있다면 unique solution 인지 어떻게 알 수 있을까요?


Span

  • The set of all linear combinations of some vectors v1, ..., vn is called span of these vector

이브가 만약 위와 같은식을 만족한다는 사실을 알고 있다면, 패스워드의 모든

]]>
http://1ambda.github.io/coding-the-matrix-2/5f630834-a38f-4833-a4a3-8d465a1c0b31Wed, 04 Mar 2015 16:28:24 GMT

Linear Combinations

bv1, ..., vn 이 주어졌을때

  • a1, ..., an 을 찾을 수 있을까요?
  • 있다면 unique solution 인지 어떻게 알 수 있을까요?


Span

  • The set of all linear combinations of some vectors v1, ..., vn is called span of these vector

이브가 만약 위와 같은식을 만족한다는 사실을 알고 있다면, 패스워드의 모든 span {a1, ..., an} 에 대해서 적절한 response 를 추출할 수 있습니다. 증명은 위처럼 간단합니다.


Let V be a set of vectors if v1, ..., vn are vectors such that V = Span {v1, ..., vn} then

  • we say {v1, ..., vn} is a generating set for V
  • we refer to the vectors v1, ..., vn as generators for V

[x, y, z] = x[1,0,0] + y[0,1,0] + z[0,0,1]R^3standard generator 라 부릅니다.


Geometry of Sets of Vectors

  • Span of the empty set: just the origin, Zero-dimensional
  • Span {[1,2], [3,4]}: all points in the plane, Two-dimensional
  • Span {[1,0,1.65], [0,1,1]} is a plain in three dimensions

k 벡터의 spank-dimensional 일까요? 아닙니다.

  • Span {[0, 0]}zero-dimensional 입니다.
  • Span {[1,3], [2,6]}one-dimensional 입니다.
  • Span {[1,0,0], [0,1,0], [1,1,0]}two-dimensional 입니다.

그러면 어떤 벡터 v 가 있을때 dimensionality 를 어떻게 알아낼 수 있을까요?

위 그림에서 볼 수 있듯이 origin 을 포함하는 geometry object 를 표현하는 방법은 두가지 입니다. 각각은 나름의 쓰임새가 있습니다.

(1) span of some vectors
(2) 우변이 0linear equation system 의 집합


field 의 서브셋은 3가지 속성을 만족합니다. fieldR 이라 하면

  • subset contains the zero vector
  • if subset contains v then it contains av for every scala a
  • if subset contains u and v then it contains u+v

F^D 의 세가지 속성을 만족하는 subsetvector space 라 부릅니다. 그리고 Uvector spacevector space Vsubset 일때, UVsubspace 라 부릅니다.

뒤에서 배울테지만 모든 R^Dsubspacespan {v1, ..., vn}{x: a1 * x = 0, ..., an * x = 0} 의 형태로 쓸 수 있습니다.


우리는 벡터에 대해 sequence 나, function 을 정의하지 않았습니다. 단순한 operator 와 공리를 만족하는지, 그리고 property V1, V2, V3 정도만 따졌습니다. 벡터에 대한 이런 추상적 접근은 많은 장점이 있습니다. 그러나 이 수업에서는 사용하지 않겠습니다.


Vector Space

벡터 c 와 벡터 스페이스 V 에 대해 c + V 와 같은 형태를 affine space 라 부릅니다.


u1, u2, u3 를 담고있는 plainu1 + V 형태로 표현하고 싶습니다. 어떻게 해야할까요?

Vspan {a, b} 라 하고 a = u2 - u1, b = u3 - u1 라 하면 u1 + Vplain 의 변환이지만, 그 자체로서 plain 입니다

  • span {a, b}0 을 포함하므로 u1 + span {a, b}u1
  • span {a, b}u2 - u1 도 을 포함하므로 u1 + span {a, b}u2
  • span {a, b}u3 - u1 도 을 포함하므로 u1 + span {a, b}u3 를 포함합니다.

따라서 u1 + span {a, b}u1, u2, u3 를 모두 포함하는 평면입니다.

더 간단히 ru1 + au2 + bu3 (r + a + b = 1) 로 affine combination 을 표현할 수 있습니다. 그리고 더 formal 하게 정의하면,


affine spacea solution set of a system of linear equations 으로 표현할 수 있습니다. 그런데, 역으로 이 솔루션이 affine space 일까요?

반례를 하나 들어보면 1x = 1, 2x = 1 일때 솔루션은 없습니다. 그러나 벡터 스페이스 Vzero vector 를 가져야 하므로 affine space u + V 는 적어도 하나의 vector 는 가져아합니다. 모순이 발생합니다.

  • Theorem: solution set of a linear systemempty 거나 affine space 입니다. 증명은 아래와 같습니다.


지금까지 증명한 것은, u1linear system 의 솔루션일때, u1 + v (v in V) 도 솔루션이란 사실입니다. 여기서 Vhomogeneous linear system 입니다. (우변이 0 인)

따라서

  • unique solution 을 가질때는 V0 을 해로 가질 때이고
  • GF(2) 의 솔루션 수는 0 이거나, V 와 같습니다.


Checksum function

corrupted 파일이 올바른 파일로 인식될 경우는 오리지널 바이너리 p 에 대해 손상된 파일 p+e 가 위 슬라이드의 방정식을 만족할 경우입니다.

이 확률은 모든 가능한 n 벡터에 대해 존재하는 솔루션의 수 이므로 굉장히 낮습니다.


Refs

(1) Title image
(2) Coding the Matrix by Philip Klein

]]>