Saltstack の highstate や low とは何なのか
最近 Saltstack を触っております。
デバッグがてらに state.show_
系のコマンドを打つことが多いのですが似たようなコマンドが多くて初見ではちんぷんかんぷんなんですよね。
state.show_sls
state.show_low_sls
state.show_highstate
state.show_lowstate
この high
や low
といった用語はあからさまに Saltstack ドメインの言葉遣いなのですが、ではそれが意味するところとは何なのか、今回はそれを押さえてより理解度深く Slatstack を使えるようになろうと思います。
といっても公式のドキュメントの簡単な和訳や要約なので特に大したことはしてません。
State System Layers
State System Layers に挙げられている項目は上から順に Saltstack における State 系の概念のローレイヤーからのスタックになっているのでそれを順に見ていきたいと思います。
Function Call
- State System における最もローレイヤーな要素
- Slatstack における State の実行とは個々の State Function の実行と言える
pkg.installed
などの関数の実行がこれに当たる
Low Chunk
- YAML(JSON?) で書かれたデータ構造
- Salt State Compiler のコンパイル結果がこれ(Low Chunk)の集積物になる
- Function Call が「実行すること」だとすると Low Chunk は 「実行するとその Function Call が実行されるもの」 と言えそう(ややこしい)
Low State
- Low Chunk が処理順に並べられたリスト
- 推測混じりで言えば、ここまでは「どの環境のどのホストで」というものを 考慮しない 純粋なデータ構造・実装や処理のことを指していると思われる*1
High Data
- SLS Files により構成される YAML として表されるデータ構造
- SLS Files をまとめたもの、という説明にとどまっている
- この時点では特に「何のためのもの」というところには触れられていない
SLS
- SLS レイヤーは High Data の論理レイヤーであると(ちょっと意味が読み取れない)
- 最終的にデータ構造さえ提供されればその生成過程は問わないとある
- 用語として SLS Files と SLS Formula は区別したいらしい
- かつては SLS は単体のファイルで構成されており SLS Formula は SLS Files とも呼ばれていた
- 現在の SLS は Pillar の内容などが適用され動的に生成されるので SLS Formula と呼ぶのが良い
- この説明の流れだと SLS Files は個々の
*.sls
ファイルを指すと考えればいいのだろうか
Highstate
- SLS を読み込んでどの minion に何を実行させるべきかを判断する
Orchestrate
- これは Salt の更に上位でやってくれとある
まとめ
ここまで読んで来た内容を今度は逆順におさらいしてみると...
- Highstate とは各 minion ごとの Low State が集まったもの
- Low State とは Low Chunk のリストで、Low Chunk とは個々の Function Call を行うためのデータ
と読み取れます。つまり当初気になっていた Slatstack 的な high/low の意味合いとしては
- 細かい個々の State 管理の項目のことが low
- 環境やグループなど minion ごとの設定を含めた全体の状態管理が high
...と呼ばれているのかな、という雰囲気はつかめたような気がします。
コマンドの解説を再度読んでみる
以上の前提知識をもとに改めて コマンドリファレンス を読んでみます。
state.show_highstate
- salt master から highstate を取得し表示する
state.show_low_sls
- SLS から Low Data 形式に落とし込まれたものを表示する
state.show_lowstate
- 指定された minion に適用される Low Data を表示する
state.show_sls
- master 上の指定された sls file の State Data を表示する
- State Data ってなんだろう...
よく見たら high/low の違いと state/sls の違いの組み合わせなんですよね。手元に試せる環境がないので何も言えないのですが、まだ違いがよくわからないんですよね... また気が向いたら今度は触りながら確認していきたいです。
*1:わかりづらい例えをすれば、我々のプロジェクトではこの筆記具のことを「鉛筆」と呼びそれはこういう構造・性質を持っています、ということを言っているのみで、それが運用上どう使われているかはこの段階では触れられていない。
TypeScript の絶対パスの import をプロジェクトルートから辿るようにする
何やら TypeScript では絶対パスでプロジェクトルートから import
を行えるようだと耳にしました。これで相対パス地獄から逃れることができますね。早速動作確認していきたいと思います。
使用した TypeScript のバージョンは 3.0.3
です。
動作確認
以下のようなディレクトリ構成で動作確認を進めていきます。
. ├── package.json ├── src │ ├── app-global-lib.ts │ ├── index.ts │ └── nested │ └── nested │ └── super-nested-lib.ts └── tsconfig.json
src/app-global-lib.ts
src 直下の src/app-global-lib.ts からは適当な定数を一つ export
します。
export const APP_GLOBAL_CONST = 1
src/nested/nested/super-nested-lib.ts
それを何階層かネストしたファイル src/nested/nested/super-nested-lib.ts から import
します。src/...
のように絶対パスで指定しています。
import { APP_GLOBAL_CONST } from 'src/app-global-lib' export const SUPER_NESTED_CONST = 2
このままだと Cannot find module
のエラーが出るのですが、ここで tsconfig.json の
"baseUrl": "./"
のコメントアウトを外すと絶対パスがプロジェクトルートからのパスとして解釈され、コンパイルが通るようになります。
src/index.ts
src/index.js では今まで通り相対パスで import
します。node_modules
のライブラリも問題なく参照できるか確認したかったので適当に uuid
を import
しています。
import { APP_GLOBAL_CONST } from './app-global-lib' import { SUPER_NESTED_CONST } from './nested/nested/super-nested-lib' import uuid from 'uuid' console.log(APP_GLOBAL_CONST + SUPER_NESTED_CONST, uuid())
これで無事 1 + 2 で 3
と UUID が表示されました。
感想
とても見やすくなりました。通常の Node.js でもできると嬉しいです。
ところで 以前 に GitHub - piotrwitek/react-redux-typescript-guide: The complete guide to static typing in "React & Redux" apps using TypeScript を読んだ時に気になったのですが
"baseUrl": "./", "paths": { "@src/*": ["src/*"] },
という設定を tsconfig.json に行うと import @src/app-global-lib
という参照の仕方もできるようなのですが、どちらがオススメなのでしょうか。
参考
- Module Resolution · TypeScript を見ると更にいろんな参照の仕方ができるようですが、今回は追いかけ切れませんでした
react-redux-typescript-guide を読んだ感触
Redux + TypeScript の検証作業で疲弊していたところで react-redux-typescript-guide を読んだら思ったより色々まとまっていて、検証のモチベーションがちょっと高まりました、という感想文です。
現在 Redux + TypeScript でアプリを作ろうと色々調べています。 業務でも趣味でも Redux を使った経験はほとんどありません。 認証情報を格納するためのストアとしてシンプルな reducer と数個の action を定義した程度です。
前回のエントリでは typescript-fsa と typesafe-actions についてごく簡単に触った感触について書きました。
dispatch に連動して副作用を実現する redux-observable や redux-thunk については何度か手元の試作プロジェクトで軽く触った程度です。 軽く触った程度ですが Redux + Typescript の荒波には十分揉まれました。 分かりやすい進捗が出せなくてそろそろ辛いです。
そして react-redux-typescript-guide というガイドを見つけました。
自分でライブラリを探している途中で何度かページ自体は目にしていたのですが、改めて読んでみたところかなり内容がまとまっているようでした。ここまで荒波にまみれてなんとなく掴めてきた理解を整理する上でも、一旦頭を空にして読む価値のありそうなガイドです。写経や読経は世俗の煩悩にまみれた状態でこそ効力を示します。
このガイドでメインで使われているのは typesafe-actions と redux-observable の組み合わせです。
今回は react-redux-typescript-guide の概要をまとめ...るようなことはせず、どの部分を読んでどういう理解になったか、何が得られたかの、ただの感想文を残していこうと思います。
コード類はほとんど引用していないので読みづらいですが済みません。
Type Definitions & Complementary Libraries
まずは補助的に使うライブラリの紹介です。 typesafe-actions はむしろメインで使うライブラリですね。
- utility-types
- Flow の Utility Types の型を TypeScript でも使えるようにするライブラリです
- こんなのあるんだと感心しました
- typesafe-actions
React Type Cheatsheet
React + TypeScript のベースとなる型情報です。この辺はある程度 React + TypeScript の経験があれば大体把握しているはずですが、それでも新しく気づいたことがあったので、網羅性というものの価値を感じます。
React.CSSPRoperties
- Material-UI でたまに JSS 用に型定義が欲しくなることがありますが React に含まれていたのは初めて知りました
Component Type Pattern
チートシートの続きで、頻出パターンの逆引き一覧になっています。ここも発見が多いです。実装の段階でお世話になりそうです。
Stateful Components
- Stateful Components って
constructor
無しでもいける defaultProps
の例はちょっとわかりづらい(というより読みづらい)
Render Props
Apollo なんかで <Query>{({ loading }) => ... }</Query>
とかやるやつですね。
そういえば Render Props と HOC の良し悪しってあまり把握してないんですよね...
Higher-Order Components
そして Higher-Order Components の書き方です。色々理解していないことだらけでした。
type
はclass
の中で定義できる- Enhanced なコンポーネントに名前をちゃんとつけててえらい
- そういえば recompose はこのガイドの中では触れられていないのが気になりました
- Error Boundary の例もあるのは嬉しいです
以前、Flow で Apollo を利用していたことがあるので、そこまで苦労しないで読めました。
bindActionCreators
Redux とのつなぎの部分で利用する bindActionCreators
(初めて知りました)でワンポイントです。
Redux の bindActionCreators
を使うときは props
内で () => void
を使うとコンパイルエラーになるけれど () => any
で代用可能、という注意書きがあり、このガイドの例もそれで実装されているみたいです。
Redux
そして来ました Redux です。 typesafe-actions を使って action を定義していきますが、 詳しくは The Mighty Tutorial を読んでね、と書いてありますね。
そして 前回 気になったところですが、リンク先にも書いてあります。 大事そうなのでここだけ原文で引用します。
WARNING: When using string constants for action type, please be sure to use simple string literals. Don't use string concatenation, template strings or object map because your type will lose the type information, widening to it's supertype string (this is how TypeScript works).
String Literal を型推論に利用しているので
template strings や object map を使うと型が string
に落ちてしまい
action type の型情報が得られなくなる(= ペイロードの型付けが行えなくなる)ということです。
大変そうですが地道に全ての action の名前をつけていくしかなさそうです。
型付けに別の手法を取っている typescript-fsa にはこういう制約はなさそうです。ここで規模のあるアプリを作成した時に、最終的な使い勝手がどう変わるかが、今後の検証でも気になるところです。
Reducers
State
の定義には readonly
を使うといいよという話。
ReadonlyArray
や utility-types の DeepReadonly
も便利そうです。
他は TypeScript のスマートな型解析で if
ステートメント内の型がその条件により type narrowing されるという話ですね。
Store Configuration
特に目立ったところはなく、以下の微妙な違いに気をつけてさえすればいいと思います。
RootAction
は全ての Action の Union 型RootState
はcombineReducer
された(構造化された) State を示す型
いちいち Root
とつけるかは好みですが、実際、プロジェクトを構造化した状態で import
を大量に行なっていたりすると、実際は何を参照しているのかあやふやになってくるので、特別な名前で参照できると安心感があります。
他に書かれているのは redux-observable の epic を middleware に仕込むところくらいです。
Async Flow
これは The Mighty Tutorial へのリンクで済まされています。これについてはまたそのうち別途書くかもしれません。
Selectors
これは完全に初見の概念だったのですが、Selector とは Store 上のあるデータに別のデータを適用した結果をキャッシュしておき、その結果が依存する State の変更があるまでそれを保持し続けることを行うライブラリの総称のようです。Computing Derived Data - Redux にも項目があります。
Vue.js の computed
に近いといえば近いのでしょうか(あれはテンプレート中で参照するためのものなので用途が違うかもしれませんが)。
Tools & Recipes
個人的に一番嬉しかったところかもしれません。よく利用されるツールの設定例が紹介されています。
- Common Npm Scripts
- tsconfig.json
@src/...
でプロジェクトルート下の./src
を参照するやり方があるのを初めて知りました- でもどうやらデフォルトで
import src/...
が動作するみたいですね... - この辺はまた検証してみたいです
- でもどうやらデフォルトで
- lib で 何を指定したらいいかの答えはここにありました(毎回迷うんですよね...)
- Vendor Types Augmentation
- ライブラリの型定義がおかしい場合に quick fix してやり過ごす方法も載っています
感想
...と、一通り読んできましたが、ただ単に読んだだけで特に手を動かして検証を行なったわけではないので、何ができるようになったではありません。それでも多くの試したいことが発見できたので、技術検証のモチベーションを高める効果はあったと思います。もしくは単に鮮度のいいネタを提供してもらえたと言うべきでしょうか。
とりあえず Redux + TypeScript にかき乱された頭の中は整流されたような気がします。 これでまた型システムの闇に立ち向かえそうな気がします。
気になったところ
ガイドを読んでいて、後で調べたいと思ったところです。
- HOC と Render Props の比較
- 両者とも同様にDI的なことを行なっているので、手法の差がどう評価されているのか気になります
- TypeScript の import path の設定
Redux + TypeScript 周りの調査
最近 Redux に手を伸ばし始めました。JavaScript で経験を積まずにいきなり TypeScript から始めたせいか無駄に試行錯誤したりハマっている気がしますが、めげずに界隈の状況把握から進めていきたいと思います。
今回は主に typesafe-actions と typescript-fsa を 1日 触った感じを軽く書いていきます。特に網羅的には書いていません。
side-effect 系ライブラリ
今回は深くは触れませんが、前提として Redux の action の dispatch に際しての副作用を提供するライブラリの代表的なところを挙げていきます。
この中では redux-thunk と redux-observable を少し使ってみました。redux-observable を利用するには RxJS をある程度触って理解することが必須となります。そして一応書けたコードを見て「これって保守性どうなのだろう」と思わされたので、もう少しこの辺は調査を進めたいところです。
しかし React の勉強をして Redux はそれとはまた別の勉強が必要になり、さらに RxJS の勉強もしないといけないとなると、新しくアサインされた人には辛いプロジェクトになるかもしれないですね(というより保守人員を求めるのであれば Redux ってどうなんでしょうね)。
action 系ライブラリ
action の定義を楽にしたり定型化したりするライブラリですね。FSA(Flax Standard Action) に基づいた設計の action を生成するものをよく見かけます。
- typescript-fsa (star: 290, npm: 9k/week, last update: 4 month)
- README に redux-observable のサンプルがある
- サポートライブラリに redux-thunk と redux-saga と redux-observable を扱うものがある
- typesafe-actions (star: 361, npm: 9k/week, last update: 3 month)
- こちらの方が微妙に勢いがある
- redux-observable のサンプルしか見つからない
という 2 つのライブラリが目につきます。勢いにも知名度にもそれほど違いがないようなので両者で迷いますね。簡単に触った感じの違いをそれぞれ見ていきます。
typescript-fsa の action 定義
const login = actionCreator.async< LoginParams, LoginInfo, ErrorResponse >('LOGIN')
のように定義します。これで login.started/done/failed
という非同期処理用の 3 種類の action が定義されます。ペイロードの type narrowing には isType が用いられ User-Defined Type Guards で実現されているみたいです。
typesafe-actions の action 定義
const login = createAsyncAction( 'LOGIN_REQUEST', 'LOGIN_SUCCESS', 'LOGIN_FAILURE' )<LoginParams, LoginInfo, ErrorResponse>();
のように定義します。これで login.request/success/failure
という非同期処理用の 3 種類の action が定義されます。typescript-fsa との違いとして 3 種類のアクション名を定義しなければならないのですが action の数が増えてくると結構面倒です。あるデータモデルのCRUD操作をしようとすると複数GETも含めて 15 個の名前を定義せねばならず、さらにそれがデータモデル分必要になってくるという。
関数なんかで生成しようとしても、現状の TypeScript には String Literal をマッピングするような操作(プレフィックスをつけた新たな String Literal を定義する等)ができず、関数を通すとただの string 型に落ちてしまうという弊害があります。typesafe-actions の type narrowing は String Literal により実現されているようなので、その恩恵にあずかるには全てのアクション名をきちんと定義する必要がありそうです。
action 定義ライブラリについての所感
パッと見の定義の楽さで言えば typescript-fsa を取りたいところですが、そこからさらに redux-thunk を使うか redux-observable を使うかの選択次第で、実は typesafe-actions が使いやすいかもしれないですし、そうでないかもしれません。
さらにこれがより実際的になってくると fetch 対象のデータモデル数が 7-8 種類程度の分量になってきたり、combineReducers
などの階層化が必要になってきたりして、その場合もちゃんと分かりやすく構造化して実装を整理できるのか、そして型定義はどうなるか、そういったこと次第でもまた選択肢が変わってくるかもしれません。
実際に typescript-fsa と typesafe-actions を行ったり来たりした感想ですが、そこまで大規模な書き換えは必要にならなかったので、現在の実装が詰み気味になり対向が気になってきたら書き換える、という方針も取れなくはないかもしれません(不安)。主に書き換えたポイントとしては以下の通りです。
- import するライブラリ
- 非同期用の action の 3 種類のフィールド名
- type narrowing の方法
ただ typescript-fsa-reducers というライブラリを使って reducer を書いていると行き来がしづらくなるので方針が固まるまでは if
で地道に reducer を書くようにとどめておくのがいいかもしれません。
...という、Redux + TypeScript + Side Effect という交差点では、やはりというか組み合わせが爆発しているのだなという、ため息に似た現状振り返りなのでした。この辺はもう、検証しながら正解スタックを探す覚悟を決めねばやっていけません。
といってもそもそも Redux をそこまでまだ理解していないので、まずはそこから、プラクティスをかき集めていきたいです。
...Apollo が懐かしいです。そう、初めは Apollo が提供している機能を「別に Redux でハンドメイドできるんじゃないか」と思って書き始めたのでした。分厚いレイヤーに嫌気がさして Redux に手を伸ばしたわけですが、自分で書くと想像以上に込み入ったコードになってしまい...フレームワークって偉大ですね、という身も蓋もない着地点が視界の端に移り始めてきたのでした。
React のファイル名から受ける想定利用法
React を触り始めて 1 年くらい経過しましたが、始めの頃はモジュールのファイル名の命名規則がよくわからなくて迷走していたのをふと思い出しました。
例えば「load data」の 2 語からなるファイル名を考えると、以下 の 3 パターンが考えられます。
load-data.js
(kebab-case)loadData.js
(lowerCamelCase)LoadData.js
(UpperCamelCase)
ある程度慣れてくると、ファイル名から作者が想定しているそのモジュールの利用法が読み取れるようになります。 今回はその個人的な感覚のまとめになります。
load-data.js から感じ取る利用法
旧来(いつだろう)の JavaScript 的には一番自然に見えるモジュールファイル名ですね。 「各メンバを個々に読み込んで利用するタイプのモジュール」と受け取れます。
import { func1, func2, const1 } from './load-data' const result1 = func1() + const1 const result2 = func2()
loadData.js から感じ取る利用法
初見で「おや、命名規則が読み取れない...」と思わされたモジュール名です。
これは「default export されている function
のメンバが存在するモジュール」と受け取れます。
import loadData from './loadData' const myData = loadData()
default export しているメンバがあるので、可能ならファイル名と同じ名前で利用してね、というメッセージを感じますね。
LoadData.js から感じ取る利用法
これも上のパターンに似ていて「default export されている class
のメンバが存在するモジュール」と受け取れます。
import LoadData from './LoadData' const loadData = new LoadData()
もしくは React などの JSX であればコンポーネントとして利用できる、という意味も追加されます。 この場合の実装はクラス・関数問わずになりますね。
import LoadData from './LoadData' function MyComponent() { return <LoadData /> }
JSX の場合は new
するクラスか、それともコンポーネントか、どちらを指すモジュールなのかは仕様や実装を見なければ判断できませんね。
Material UI + TypeScript で withStyles (JSS) を型付きで使う
Material-UI を使い始めて数日が経ち、ようやくJSSの思想が飲み込めてきました。 当初の違和感もだんだん薄れ、今では(今のところは)確かに楽だよねという感覚になってきております。
今回は型定義についてです。
import * as React from 'react' import { createStyles, Theme, WithStyles, withStyles } from '@material-ui/core/styles' interface Props extends WithStyles<typeof styles> { // my props... } function MyComponent({ classes }: Props) { return ( <div className={classes.root}> {/* my contents... */} </div> ) } const styles = createStyles({ root: { // my styles... }, }) export default withStyles(styles)(MyComponent)
と定義すると classes.xyz
のように styles
変数の定義に存在しない ClassKey
にアクセスしようとするとエディタが怒ってくれるようになります。
typeof styles
で styles
オブジェクトを型の世界に持ち込んでいるのがみそですね。
関数形式の styles
を利用する場合は以下のようにします。
const styles = ({ spacing }: Theme) => createStyles({ root: { // my styles... }, })
始めは createStyles((theme: Theme) => ({ ... }))
と定義しようと勘違いしておりました。
そして後から気づいたのですが、今回の内容は全部 https://material-ui.com/guides/typescript/ に書いてありましたね。
Material UI + TypeScript で Mixin を定義する
TypeScript + Materia UI でアプリを書いていて、テーマ関係のメソッドの中に createMixins
というものを見つけたので使い方を調べてみました。
デフォルトの mixin といえば gutter
くらいしかなくて、自分でも登録できたら色々使えそうだな...と思い、型定義を追いかけながらコードを試行錯誤したところ、多少不恰好ですが動くものができたのでメモしておきます。
プロジェクトコードは以下のリポジトリにあります。
プロジェクト作成
プロジェクトは create-react-app
の react-scripts-ts
で生成したものをベースにしています。この辺は特に詳細は説明しません。
create-react-app practice-mui-with-jsonschema-form --scripts-version=react-scripts-ts --use-npm
Theme の定義
Theme
を定義しているファイルの全体のコードは以下のようになります。
import { createMuiTheme } from '@material-ui/core/styles' import createMixins from '@material-ui/core/styles/createMixins' import { CSSProperties } from '@material-ui/core/styles/withStyles' // `Mixins` 定義を拡張する declare module '@material-ui/core/styles/createMixins' { interface Mixins { myBackgroundMixin: CSSProperties, // <- custom mixin! } } // `createMixins` で利用するインスタンスを取得する const { breakpoints, spacing } = createMuiTheme() const mixins = createMixins(breakpoints, spacing, { myBackgroundMixin: { backgroundColor: '#ff0000' } }) export default createMuiTheme({ mixins, palette: { divider: '#ff0000' }, })
以下、個々のポイントを説明していきます。
Mixin の定義を拡張する
createMixins
で参照されている interface Mixins
の定義を見ていると何やら意味深なコメントが書いてあります。
export interface Mixins { gutters: (styles?: CSSProperties) => CSSProperties; toolbar: CSSProperties; // ... use interface declaration merging to add custom mixins }
TypeScript の柔軟であり分かりづらい部分ですが、interface
などの定義は後追い拡張(マージ)が行えます。
ここでは以下のように myBackgroundMixin
を追加定義してみました。
declare module '@material-ui/core/styles/createMixins' { interface Mixins { // (=˘ ꒳ ˘=) 展開先を赤く染め上げる血みどろの Mixin... myBackgroundMixin: CSSProperties, } }
Mixin の定義を行う
これにより createMixins
の引数である MixinsOptions
にも拡張が反映され、createMixins
内で myBackgroundMixin
が定義できるようになります
const mixins = createMixins(breakpoints, spacing, { // (=˘ ꒳ ˘=) 血みどろの実装...容赦のないビビッドな赤 myBackgroundMixin: { backgroundColor: '#ff0000' } })
上の定義の拡張を行わないと Object literal may only specify known properties, and 'myBackgroundMixin' does not exist in type 'MixinsOptions'.
というエラーが出ます。
ちょっと曲者な createMixins の引数
createMixins
の引数には Breakpoint
と Spacing
のインスタンスが必要なのですがいちいち用意したくはないですよね。今回は小手先のアイディアとして一度 createMuiTheme()
を実行してデフォルトのインスタンスを取得しています。
const { breakpoints, spacing } = createMuiTheme()
もっといい方法がある気がしますね。
実際に利用してみる
上記の定義を含めた Theme
インスタンスを MuiThemeProvider
経由で提供してやれば、配下のコンポーネントで今回定義した myBackgroundMixin
を利用することができます。
import * as React from 'react' import Button from '@material-ui/core/Button' import { createStyles, Theme, withStyles, WithStyles } from '@material-ui/core/styles' interface Props extends WithStyles<typeof styles> { name: string } function MyButton({ classes, name }: Props) { return ( <Button className={classes.root}> {name} </Button> ) } const styles = ({ mixins, palette }: Theme) => createStyles({ root: { ...mixins.myBackgroundMixin, color: palette.common.white, } }) export default withStyles(styles)(MyButton)
無事、手元では赤いボタンが表示されました。
ここまで、細切れの説明になりましたので、冒頭で紹介したリポジトリで全体を見ていただくのが分かりやすいかもしれません。