My beef with Copilot

I feel that a lot of time Copilot and I do not agree on F# code. Recently, I had the following discussion about this code file:

OrderVariable Duplication Conversation

1. User:
In terms of intellisense, you type Quantity. and all the functions are provided that you can apply to a Quantity. Is there another way to achieve this without you complaining about duplicate code and bad programming practice?

Copilot:
Yes, you can achieve the same intellisense experience without code duplication by using a combination of:
- A generic record of functions (a "module dictionary" or "typeclass-like" pattern).
- A single generic module that exposes all the operations, parameterized by the record.
- Type-specific modules that only provide the record instance and re-export the generic functions.

This way, intellisense still works (e.g., Quantity.toDto, Quantity.clear), but the implementation is shared.

Example:

// Shared generic operations
module OrderVariableOps =
type Ops<'a> = {
toOrdVar: 'a -> OrderVariable
wrap: OrderVariable -> 'a
toDto: 'a -> Dto.Dto
fromDto: Dto.Dto -> 'a
clear: 'a -> 'a
// ... add more as needed
}

let toDto ops x = ops.toDto x
let fromDto ops dto = ops.fromDto dto
let clear ops x = ops.clear x
// ... etc.

// Type-specific module
module Quantity =
let ops = {
OrderVariableOps.Ops.toOrdVar = (fun (Quantity qty) -> qty)
wrap = Quantity
toDto = (fun x -> x |> (fun (Quantity qty) -> qty) |> Dto.toDto)
fromDto = (fun dto -> dto |> Dto.fromDto |> Quantity)
clear = (fun x -> x |> (fun (Quantity qty) -> qty) |> OrderVariable.clear |> Quantity)
// ... etc.
}

let toDto = OrderVariableOps.toDto ops
let fromDto = OrderVariableOps.fromDto ops
let clear = OrderVariableOps.clear ops
// ... etc.

Result:
- You get Quantity.toDto, Quantity.clear, etc., in intellisense.
- All logic is shared in one place.
- No code duplication.

This is a common F# pattern for "typeclass-like" behavior with good IDE support.

---

2. User:
But then you will have for each submodule, Count, Quantity etc.. an "ops" and thus duplicate code, i.e. amounting to what is currently implemented?

Copilot:
The ops record in each submodule (e.g., Count, Quantity) is a small, type-specific value that wires up the generic logic to the concrete type. While you do define an ops per type, this is not the same as duplicating all the function implementations.

Key differences:
- Current approach: Each submodule repeats the full implementation of every function, leading to large-scale duplication.
- With ops pattern: Only a single small record (the ops value) is defined per type, and all function logic is shared. The per-type code is minimal and declarative.

Example:

module Quantity =
let ops = {
toOrdVar = (fun (Quantity qty) -> qty)
wrap = Quantity
// ...other mappings
}
let toDto = OrderVariableOps.toDto ops
let clear = OrderVariableOps.clear ops
// etc.

This is a standard F# pattern for typeclass-like behavior. The only "duplication" is the one-liner wiring per type, not the function bodies. This is much more maintainable and idiomatic.

---

3. User:
But in each ops you define the same exact functions that are now implemented on a module level.

Copilot:
You are correct: with the ops record pattern, you still repeat the wiring of the same functions (e.g., toOrdVar, wrap, etc.) for each type. However, this duplication is much smaller and more declarative than repeating the full function bodies in every module.


## 4. Is the ops Record Just Duplication?

**User:**
But then you will have for each submodule, Count, Quantity etc. an "ops" and thus duplicate code, i.e. amounting to what is currently implemented?

**Copilot:**
The ops record in each submodule is a small, type-specific value. This is not the same as duplicating all function implementations. The per-type code is minimal and declarative.

---

## 5. Is the ops Record Still Duplication?

**User:**
But in each ops you define the same exact functions that are now implemented on a module level.

**Copilot:**
You are correct: with the ops pattern, you still repeat the wiring of the same functions per type. However, this duplication is much smaller and more declarative than repeating the full function bodies in every module. It is a trade-off for type-safe, discoverable APIs with intellisense.

---

## 6. Context: Code Excerpts

The conversation references code from:
- src/Informedica.GenSolver.Lib/Variable.fs
- src/Informedica.GenOrder.Lib/OrderVariable.fs

The OrderVariable file contains many type-specific modules (e.g., Count, Quantity, Time, etc.) that repeat similar function patterns, which could be reduced using the discussed generic patterns.

---

Loading

Leave a Reply

Your email address will not be published. Required fields are marked *