# Scalar vs 0-d array
###### tags: `design` `discussion`
## Meeting 2022-09-27
### Summary (written retrospectively)
(see [below](#scratch-pad) for the snippets made during the meeting)
We decided to limit the discussion to only `field_operator`s first, see [Scope of the discussion](#Scope-of-the-discussion), as discussion should be the easiest there (we are clearly inside GT4Py, no discussion of how view changes at the entry-point to GT4Py).
The whole (2h) discussion was around the question "Does it make sense to distinguish between a 0-d field and a scalar?" and evolved towards the questions:
- How can we express that they are aquivalent without violating plain-Python semantics of type annoations (and their behavior in type checkers)? See [below](#How-can-we-make-float-and-Field-float-aquivalent).
- Only touched on: are these techniques worth the effort.
We extensively discussed the 4 possible combinations of type annotations for a scalar with the following example
```python=
@field_operator
def foo(bar: T0) -> T1
return bar
```
where `T0` and `T1` can be either `float` (scalar) or `Field[[], float]` (0-d field of float).
In the following the discussion around each of the individual cases is summarized (if a type annotation is `T`, it indicates that for the argument made, it doesn't matter which annotation should be used)
**(Field[[], float]) -> Field[[], float]**
```python=
@field_operator
def foo(bar: Field[[], float]) -> Field[[], float]
return bar
```
No discussion, we all agree that this makes sense.
**float -> T**
```python=
@field_operator
def foo(bar: float) -> T
return bar
```
We came to the agreement that within the body of a `field_operator`, we can think of all operations as field operations ("everything is a field"). Therefore, allowing to describe arguments as scalar doesn't make sense for semantics, but is convenient for syntax.
Further, we expect that the function that is being decorated will get only Fields (the decorator will wrap arguments in objects with `Field` semantics).
The above annotation is therefore technically wrong as we are type-annotating the function that is being decorated (which will only get `Field`s). But we could [make it correct](#How-can-we-make-float-and-Field-float-aquivalent).
**(T) -> float**
```python=
@field_operator
def foo(bar: T) -> float
return bar
```
Because of the previous point, this type-annotation is technically wrong, because bar will always be a Field. It can be fixed with the same techniques.
**() -> Field[[], float]**
```python=
@field_operator
def foo() -> Field[[], float]
return 1.0 # this is a scalar literal
```
This is a corner-case where no field object is involved. It is technically wrong, because a float literal is not a Field. However, on the caller side, we can ensure (in the `field_operator` decorator) that returned value of the call to foo (after decoration) has `Field` semantics.
This leads to the interesting discussion of [What are we type-annotating?](#What-are-we-type-annotating).
#### Other discussion points
##### What are we type-annotating?
```python=
@decorator
def foo(arg: annotation) -> result_annotation
...
```
The type annotations are for the function `foo` before the decorator is applied. That is unfortunate for our use-case. (There was a discussion in https://github.com/python/typing/issues/412, but even if that would have succeeded the solution wouldn't have been nice for a DSL).
What we ideally would like is that the user annotates the field operator, not the decorated function. The type annotation for the decorated function is basically useless for the reader.
#### How can we make `float` and `Field[[], float]` aquivalent
##### Custom scalar types
Instead of allowing the Python builtin types (e.g. `float`), we introduce our own types which can provide Field semantics. TODO expand here with some details
TODO expand also on the following and add details
```python=
scalar: gt4py.float = 4.0
assert isinstance(scalar, gt4py.float)
assert isinstance(scalar, Field[[], gt4py.float])
assert scalar.dtype == gt4py.float
assert scalar.dtype == Field[[], gt4py.float]
assert scalar.dtype.dtype.dtype.dtype == gt4py.float
assert scalar.dtype.dtype.dtype.dtype == Field[[], gt4py.float]
```
##### ABC
register Python builtins as subclass of Field
TODO expand
```python=
scalar: float = 4.0 # `float` is the Python builtin
assert (scalar, Field[[], float])
```
### scratch pad
#### Conclusion
- it's enough that we think of everything as fields in the body of the field_operator
- the function that is decorated gets only fields
```python
@field_operator
def foo(field: float, field2: Field[[], float]) -> float:
return field
@field_operator
def bar(field: Field[[], float]):
return foo(field)
```
```python
@field_operator
def foo(field: float) -> Field[[], float]:
# assert isinstance(field, Field[[], float])
# field.shape
assert field.dtype == Field[[], float]
assert field.dtype == float
return field # currently need `return broadcast([], field)`
```
```python
@field_operator
def foo(field: Field[[], float]) -> float:
return field
```
```python
@field_operator
def foo(field: Field[[], float]) -> Field[[], float]:
return field
```
---
```python
@field_operator
def foo(field: float, offset: int) -> Field[[], float]:
return field(Dim[offset+1])
```
```python=
@fundef
def stencil(field: iterator, offset: iterator)
return deref(shift(Dim, deref(offset)+1)(field))
```
## Scope of the discussion
- Where does GT4Py field view begin?
```python=
# outside-of-gt4py-scope
out = make_a_located_field(...)
scalar = 1.0
# field_operator-scope
@field_operator
def foo(field):
return shift(Dim, 1)(2)
# program-scope
@program
def bar(scalar: ..., field: ...)
foo(scalar, out=field)
# outside-of-gt4py-scope
bar(scalar, out)
```
## Hannes unstructured notes (prior to the meeting)
### Enrique and Hannes conclusions after discussing the summary
At field_operator level we make the distinction between scalar and 0-d field. At iterator IR level, the lowered field_operator should take only iterators. (E.g., before we lower we promote the stuff that should be iterator to 0-d field.)
#### Examples
The following is forbidden because you annotate the function that is being decorated.
```python=
@field_operator
def foo() -> Field[[], float]
return 1.0
```
Optionally, we could add a explicit broacast option to the decorator:
```python=
@field_operator
def foo() -> float:
return 1.0
foo: Callable[[], float]
@field_operator(broadcast_to=Field[[], float])
def foo_with_broadcast() -> float:
return 1.0
foo_with_broadcast: Callable[[], Field[[], float]]
```
The following is forbidden because ternaries only work with scalars
```python=
@field_operator
def foo(cond: Field[[], int]):
return some_field_op() if cond == 1 else some_other_field_op()
```
In the following call an implicit broadcast to Field is performed in the decorator
```python=
@field_operator
def foo(scalar: Field[[], float]):
return cond
foo(1.0)
```
but the following is legal and no broadcast is performed
```python=
@field_operator
def foo(scalar: float) -> float:
return scalar
foo(1.0)
```
### Hannes' interpretation
float == Field[[], float]
```python=
def foo(a: float):
assert is_scalar(a)
assert is_field(a)
assert rank(a) == 0
```
### Are we discussing semantics or implementation?
### Problems at the entrypoint to GT4Py
#### scalars as out arg in field view
>from https://mail.python.org/pipermail/numpy-discussion/2006-February/006384.html: rank-0 array cannot be replaced by an array scalar because it is mutable.
```python=
@program
def foo(scalar: float):
bar(out=scalar) # not possible``
```
#### embedded execution
Either
- scalars need to be wrapped at the entrypoint to provide field operations, or
- all operations need to be overloaded for scalar and field operations
#### Comparison to NumPy discussion
For numpy there is not entrypoint, we are always dealing with the user provided objects: a `float` (scalar) doesn't have `.shape` member, while a 0-d field has.
Does GT4Py have an entrypoint? Most likely there is no way to avoid some logic between field_operator/programs and the user calling them. Here we can easily canonicalize user objects.
Small caveat: we cannot write to a scalar, but only to a 0d-field.
### Iterator IR
#### Stencils take iterators, otherwise lift doesn't work
### Corner cases
```python=
@field_operator
def foo(cond: ?):
return some_field_op() if cond == 1 else some_other_field_op()
```
- could be lowered as where
- iterator ir currently requires an iterator