8

楽しみのためにポイントフリー スタイルのJavaScript をいじっています。

ビデオ ゲームのディアブロをコーディングしていて、次のような複雑なネストされた型を使用して敵をモデル化しているとしますが、より深く複雑です。

{ name: "badguy1", stats: { health: 10: strength: 42 }, pos: {x: 100, y: 101 } }

だから私はすべての敵のリストを持っています。一定範囲内の敵全員にダメージを与えたい

function isInRange(radius, point) { return point.x^2 + point.y^2 >= radius^2; }
function fireDamage(health) { return health - 10; }    
var newEnemies = enemies.filter(isInRange).map(fireDamage);

もちろん、これは型チェックではありません - 私のコンビネータはプリミティブを取るので、「別のレベルで」マップしてフィルタリングする必要があります。フィルター/マップのビジネス ロジック パイプラインをわかりにくくしたくありません。私はレンズが私を助けることができることを知っていますが、これはもちろん変更可能な構造では些細なことなので、ブラウザにいるとしましょう。どうすればいいのですか?

4

2 に答える 2

7

あなたの質問は、Javascript でレンズを使用する方法についてですか? もしそうなら、私は助けることができるかもしれません。Ramda.js ライブラリをチェックアウトしましたか? これは関数型 JS を作成する素晴らしい方法です。まず、敵のモデルを見てみましょう。

/* -- data model -- */
let enemyModel = {
  name: "badguy1",
  stats: {
    health: 10,
    strength: 42
  },
  pos: {
    x: 100,
    y: 101
  }
};

レンズ:レンズを作成するには、特定のオブジェクト (この場合は「敵」) のゲッター メソッドとセッター メソッドが必要です。これらを手動で作成する方法は次のとおりです。

方法 1:独自のゲッターとセッターを作成する

const getHealth = path(['stats', 'health']);
const setHealth = assocPath(['stats', 'health']);
const healthLens = lens(getHealth, setHealth);

方法 2:オブジェクトに対する Ramda の都合のよい便利なレンズ

const healthLens = lensPath(['stats', 'health']);

レンズを作成したら、それを使用します。Ramda は、レンズを使用するための 3 つの関数を提供します: view(..)set(..)、およびover(..)

view(healthLens)(enemyModel); // 10
set(healthLens, 15)(enemyModel); // changes health from 10 to 15
over(healthLens, fireDamage)(enemyModel); // reduces enemyModel's health property by 10

この機能を敵の体力に適用しているのでfireDamage(..)、 を使用する必要がありますover(..)。また、位置座標はenemyModel内にネストされているため、レンズを使用してそれらにアクセスすることもできます。それを作成して、その間にリファクタリングisInRange(..)しましょう。

参考までに、元の fn は次のとおりです。

// NOTE: not sure if this works as you intended it to...

function isInRange(radius, point) {
  return point.x^2 + point.y^2 >= radius^2; // maybe try Math.pow(..)
}

機能的なアプローチは次のとおりです。

/* -- helper functions -- */
const square = x => x * x;
const gteRadSquared = radius => flip(gte)(square(radius));
let sumPointSquared = point => converge(
  add,
  [compose(square, prop('x')), 
   compose(square, prop('y'))]
)(point);
sumPointSquared = curry(sumPointSquared); // allows for "partial application" of fn arguments

/* -- refactored fn -- */
let isInRange = (radius, point) => compose(
  gteRadSquared(radius),
  sumPointSquared
)(point);
isInRange = curry(isInRange);

これは、enemyModels のコレクションを扱う場合にどのようになるかを示しています。

/* -- lenses -- */
const xLens = lensPath(['pos', 'x']);
const yLens = lensPath(['pos', 'y']);
const ptLens = lens(prop('pos'), assoc('pos'));

// since idk where 'radius' is coming from I'll hard-code it
let radius = 12;

const filterInRange = rad => filter(
  over(ptLens, isInRange(rad))  // using 'ptLens' bc isInRange(..) takes 'radius' and a 'point'
);
const mapFireDamage = map(
  over(healthLens, fireDamage)  // using 'healthLens' bc fireDamage(..) takes 'health'
);

let newEnemies = compose(
  mapFireDamage,
  filterInRange(radius)
)(enemies);

これが、レンズがいかに役立つかを説明するのに役立つことを願っています. 多くのヘルパー関数がありますが、コードの最後の部分は非常にセマンティックだと思います!

最後に、この例を読みやすくするために、Ramda のこれらの関数でスコープをあふれさせています。これを達成するためにES6の分解を使用しています。方法は次のとおりです。

const {
  add,
  assocPath,
  compose,
  converge,
  curry,
  filter,
  flip,
  gte,
  lens,
  lensPath,
  map,
  over,
  set,
  path,
  prop,
  view
} = R;

// code goes below...

jsBin で試してみてください。Ramda サポートを提供します。

于 2016-03-02T21:08:54.083 に答える
6

レンズに関する私の記事を読んでください。それはあなたがそれを言葉で表現した方法で正確にあなたの質問に答えます. 真剣に、私は冗談でもありません。これが私の投稿のコードスニペットです:

fireBreath :: Point -> StateT Game IO ()
fireBreath target = do
    lift $ putStrLn "*rawr*"
    units.traversed.(around target 1.0).health -= 3
于 2013-09-04T03:38:14.937 に答える