# `Latu.ML.Result`
[🔗](https://github.com/zero-one-group/latu_ml/blob/v0.4.2/lib/latu/ml/result.ex#L1)

Read what an `MlCommand` answered.

Latu latches the whole `MlCommandResult` onto the finished execution rather than picking an
arm, because which arm is set is part of the answer. This is where the picking happens.

Clauses match maps rather than named messages, as `Latu.Result.Literal` does: the shape is
what matters, and a pattern is not a call into the protocol layer.

# `operator_info`

```elixir
@type operator_info() :: %{
  ref: String.t() | nil,
  name: String.t() | nil,
  uid: String.t() | nil,
  params: [Latu.ML.Plan.param()]
}
```

A loaded operator, as a `Read` described it.

`ref` is set for a model and nil for everything else; `name` is the reverse. `params` are
the values the saved metadata carried, in the shape `Latu.ML.Plan.params/1` takes them.

# `object_ref`

```elixir
@spec object_ref(struct()) ::
  {:ok, Latu.ML.Plan.object_ref()} | {:error, Latu.Error.t()}
```

The object reference a `Fit` put in the ML cache.

A `Fit` populates `obj_ref` and nothing else — `uid`, `params` and `warning_message` all come
back nil — which is why a model's uid is the client's own, not the server's.

# `operator_info`

```elixir
@spec operator_info(struct()) :: {:ok, operator_info()} | {:error, Latu.Error.t()}
```

What a `Read` answered with: a loaded operator, as data.

The one answer the server fills in. A `Fit` populates `obj_ref` and nothing else — a fitted
model's uid and params are the client's own, copied from the estimator — where this carries
`uid` and `params` too, because both came off the saved metadata and no client could have
known them.

A **model** carries a cache reference as well: reading one registers it in the session's ML
cache exactly as a fit does, and it is the caller's to release. An estimator, transformer or
evaluator carries its class name instead, and nothing is cached.

Each param is typed from the arm the server chose rather than from the registry, so a class
the registry never heard of loads as readily as one it knows. The two agree everywhere they
both apply: PySpark re-types a loaded param through its declared converter, and every
disagreement that could produce — a `:float` param, an `:int` past 32 bits — collapses to
the same literal on the way back out.

# `operators`

```elixir
@spec operators(struct()) ::
  {:ok, [Latu.ML.Plan.object_ref()]} | {:error, Latu.Error.t()}
```

The cache ids a `Fetch` answered with, where the answer was a *list* of cached objects.

`MlCommandResult`'s `operator_info` arm again, but the server has packed several ids into one
`ObjectRef` by joining them with commas — `MLHandler` does `ids.mkString(",")` and PySpark
splits them apart. There is no repeated field to read instead, so this is the format rather
than a convention.

Each id is a **separate cache entry**: the server registered every sub-model on its way out,
so a forest of a hundred trees is a hundred entries the caller now owns.

# `param`

```elixir
@spec param(struct()) :: {:ok, term()} | {:error, Latu.Error.t()}
```

The value an attribute `Fetch` answered with.

A `Vector` or `Matrix` arrives as a UDT-typed struct literal and comes back as an
`Nx.Tensor` or a `Latu.ML.SparseVector`; everything else is whatever
`Latu.Result.Literal.value/1` makes of it.

# `summary`

```elixir
@spec summary(struct()) :: {:ok, String.t()} | {:error, Latu.Error.t()}
```

The cache id a `Fetch` answered with, where the answer was another cached object.

`MlCommandResult`'s third arm, and a bare string: asking a model for its `summary` gets the
id of the summary rather than the summary itself, because a summary is a server-side object
like the model is. `Latu.ML.summary/1` does not send this — it composes the reference from
the model's own, as PySpark does — so this is here for the paths that do, `evaluate` first
among them.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
