Skip to content

Commit 594ba8b

Browse files
authored
Merge pull request #74 from input-output-hk/mgalazyn/adr-use-inject-for-conversion-functions
ADR: Total conversion functions' conventions
2 parents 6b2d51f + 8cb5335 commit 594ba8b

File tree

1 file changed

+118
-0
lines changed

1 file changed

+118
-0
lines changed
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# Status
2+
3+
- [ ] Proposed 2024-05-21
4+
5+
# Context
6+
7+
In `cardano-api` we have multiple functions for performing conversions on values from one type to another, for example:
8+
9+
```haskell
10+
fromShelleyDeltaLovelace :: L.DeltaCoin -> Lovelace -- 'from' at the beginning
11+
lovelaceToQuantity :: Lovelace -> Quantity -- 'to' in the middle
12+
toAlonzoScriptLanguage :: AnyPlutusScriptVersion -> Plutus.Language -- 'to' at the beginning
13+
convReferenceInputs :: TxInsReference build era -> Set Ledger.TxIn -- 'conv' at the beginning
14+
```
15+
16+
There are multiple naming conventions for the conversion functions which makes them hard to locate.
17+
Some conversion functions with lengthy names, are not very convenient to use.
18+
19+
# Decision
20+
21+
## Type classes
22+
23+
For total functions, which are simply converting a value from one type to another, we can use type classes [`Inject` (from `cardano-ledger`)](https://cardano-ledger.cardano.intersectmbo.org/cardano-ledger-core/Cardano-Ledger-BaseTypes.html#t:Inject) & [`Convert`](https://cardano-api.cardano.intersectmbo.org/cardano-api/Cardano-Api-Internal-Eras.html#t:Convert):
24+
```haskell
25+
class Inject t s where
26+
inject :: t -> s
27+
28+
class Convert (f :: a -> Type) (g :: a -> Type) where
29+
convert :: forall a. f a -> g a
30+
```
31+
32+
The use of those conversion functions should be limited to **internal use only**.
33+
The library should still export conversion functions with explicit type names for better readability.
34+
An exception to this would be a set of types which are all convertible to each other, like `Eon`s.
35+
Writing $N \times N$ conversion functions for $N$ types would be cumbersome, so using `inject`/`convert` instead is justified here.
36+
37+
Inject instances should be placed near the definition of one of the types, to make them more discoverable and avoid orphaned instances.
38+
39+
>[!NOTE]
40+
>The difference between `Inject` and `Convert` class is that `Convert` is better typed for types with `Type -> Type` kind.
41+
>In other words, when writing `instance Inject (Foo a) (Bar a)` the GHC's typechecker needs some help to understand the code using `inject`:
42+
>```haskell
43+
>let x = inject @_ @(Bar Bool) $ Foo True
44+
>```
45+
>That is not needed for `convert`.
46+
47+
### Injection law
48+
49+
The `Inject` and `Convert` classes are meant to be used for trivial conversions only and not for more complex types like polymorphic collections (e.g. `[a] -> Set a` which loses ordering).
50+
The `inject` and `convert` implementations should both be injective:
51+
```math
52+
\forall_{x,x' \in X} \ \ inject(x) = inject(x') \implies x = x'
53+
```
54+
55+
This effectively means that any hashing functions or field accessors for constructors losing information (e.g. `foo (Foo _ a) = a`) should not be implemented as `Inject`/`Convert` instances.
56+
57+
## Explicit conversion functions
58+
59+
For explicit conversion functions, the following naming convention should follow:
60+
61+
```haskell
62+
fooToBar :: Foo -> Bar
63+
```
64+
65+
>[!IMPORTANT]
66+
>Conversion functions should be placed near the conversion target type definition if possible.
67+
68+
### Qualified imports
69+
70+
If the module exporting conversion functions is meant to be imported qualified, and provides functions for operating on a single data type, a shorter name with `from` or `to` prefix is allowed.
71+
72+
#### `from`-prefixed functions
73+
74+
For defining conversion functions, **using `from`-prefixed functions should be preferred**.
75+
The `from...` function should be placed nearby the `Foo` definition.
76+
77+
```haskell
78+
module Data.Foo where
79+
80+
import Data.Bar (Bar)
81+
82+
data Foo = Foo
83+
84+
fromBar :: Bar -> Foo
85+
```
86+
87+
where the usage would look like:
88+
```haskell
89+
import Data.Foo qualified as Foo
90+
91+
Foo.fromBar bar
92+
```
93+
94+
#### `to`-prefixed functions
95+
96+
When it's not possible to define `from`-prefixed functions in the location of the target type, it's permitted to use `to`-prefixed function.
97+
98+
```haskell
99+
module Data.Foo where
100+
101+
import Data.Baz (Baz)
102+
data Foo = Foo
103+
104+
toBaz :: Foo -> Baz
105+
```
106+
107+
# Consequences
108+
109+
## Advantages
110+
- An uniform API for total conversions
111+
- A list of `Inject` instances lists all available conversions for the type
112+
- Less maintenance burden with regards to the naming conventions of the conversion functions
113+
114+
## Disadvantages
115+
- It may be a bit less obvious how to discover available conversions, because one would have to browse the type's `Inject` instances to find the conversion functions they are looking for - instead of looking for exported functions.
116+
117+
118+
[modeline]: # ( vim: set spell spelllang=en: )

0 commit comments

Comments
 (0)