Skip to content

Commit ea7ef24

Browse files
committed
ADR: Total conversion functions conventions
1 parent 6b2d51f commit ea7ef24

File tree

1 file changed

+93
-0
lines changed

1 file changed

+93
-0
lines changed
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Status
2+
3+
- [ ] Proposed 2024-05-21
4+
5+
# Context
6+
7+
## The Problem
8+
In `cardano-api` we have multiple functions performing conversions between one value of the type to the other, for example:
9+
10+
```haskell
11+
fromShelleyDeltaLovelace :: L.DeltaCoin -> Lovelace -- 'from' at the beginning
12+
lovelaceToQuantity :: Lovelace -> Quantity -- 'to' in the middle
13+
toAlonzoScriptLanguage :: AnyPlutusScriptVersion -> Plutus.Language -- 'to' 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+
## Solution Proposal
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 is limited to **internal use only**.
33+
The library should still export conversion functions with explicit type names for better readability.
34+
Inject instances should be placed near one of the types definition, to make them more discoverable and avoid orphaned instances.
35+
36+
>![NOTE]
37+
>The difference between `Inject` and `Convert` class is that `Convert` is better typed for rank 1 types.
38+
>In other words, when writing `instance Inject (Foo a) (Bar a)` the GHC's typechecker needs some help to understand the code using `inject`:
39+
>```haskell
40+
>let x = inject @_ @(Bar Bool) $ Foo True
41+
>```
42+
>That is not needed for `convert`.
43+
44+
#### Injection law
45+
46+
The `Inject` and `Convert` classes are meant to be used for trivial conversions only and not for more complex types like collections (e.g. `Set a`).
47+
The `inject` and `convert` implementations should be injective:
48+
```math
49+
\forall_{x,x' \in X} \ \ inject(x) = inject(x') \implies x = x'
50+
```
51+
52+
This 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.
53+
54+
### Explicit conversion functions
55+
56+
For explicit conversion functions, the following naming convention should follow:
57+
58+
```haskell
59+
fooToBar :: Foo -> Bar
60+
```
61+
62+
#### Qualified imports
63+
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` prefix is allowed:
64+
65+
```haskell
66+
module Data.Foo where
67+
68+
fromBar :: Bar -> Foo
69+
```
70+
71+
where the usage would look like:
72+
```haskell
73+
import Data.Foo qualified as Foo
74+
75+
Foo.fromBar bar
76+
```
77+
78+
# Decision
79+
80+
TBD
81+
82+
# Consequences
83+
84+
## Advantages
85+
- An uniform API for total conversions
86+
- A list of `Inject` instances lists all available conversions for the type
87+
- Less maintenance burden with regards to the naming conventions of the conversion functions
88+
89+
## Disadvantages
90+
- It may be a bit surprising 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.
91+
92+
93+
[modeline]: # ( vim: set spell spelllang=en: )

0 commit comments

Comments
 (0)