One thing is not obvious to me. How an application or lib should behave when a developer is going to break its state. Not everything can be type-checked. In my case, the user can select some text out of boundaries depending on some typed JSON structure. With Option, we can fail silently. Sure. But in my case, I prefer to fail soon fail cheap approach. Technically, it should not happen unless a developer will call some fn with the wrong value.
I believe in such case it's OK to throw probably dev only error. Am I correct?
Otherwise, I would have to use Option everywhere in my app, and I believe Option should be mainly for function result, not for function arguments. Am I correct?
Can you post an example? Ideally, you'd make sure not to break the state. fp-ts has a companion library (io-ts) that can check at runtime your inputs are correct.
@steida I think it is consense in fp-ts to never throw. The only exception is the absurd function that is needed for some special cases for example when you want to construct a function that should never return.
When I understand you correct this is the question of how to integrate external systems (which can also be the user using your application) in your well typed application. First rule of thumb everything that comes from an untyped system should be validated with io-ts this should be the first line of defense then it is up to you how you want to handle this error. If you want to fail fast and cheap start with a simple Either<Error, MyReturnType> construct and convert all errors returned from your untrusty APIs to the Error type. I would recommend the usage of IOEither and TaskEither for your effectful code depending on your async needs. Then you can use the orElse at the end of your application to simulate a catch-all clause in your code.
After this you can start designing your errors. I use ts-union to distinct between possible types of errors.
const ApiError = Union({
ValidationErrors: of<t.Errors>(),
HttpError: of<HttpError>(),
UnknownError: of<Error>()
})
Otherwise, I would have to use Option everywhere in my app, and I believe Option should be mainly for function result, not for function arguments. As I correct?
Thats correct. Use sequenceT or sequenceS for cases where you have Option values to lift your function.
@pigoz Check setText function. Of course, it can silently fail, but I would rather tell a developer why the app "does nothing" because I believe it's better for DX. Maybe it should silently fail in production, I am not sure.
export type EditorNodeID = Brand<string, 'EditorNodeID'>;
export interface EditorNode {
readonly id: EditorNodeID;
}
export interface EditorText extends EditorNode {
readonly text: string;
}
export interface EditorElement extends EditorNode {
readonly children: (EditorElementChild)[];
}
export type EditorElementChild = EditorElement | EditorText;
export interface EditorState {
readonly element: EditorElement;
readonly selection: Option<EditorSelection>;
readonly hasFocus: boolean;
}
export function setText(text: string): Endomorphism<EditorState> {
return state => {
// TODO: Replace toNullable with something.
const selection = toNullable(state.selection);
// https://github.com/gcanti/fp-ts/issues/973
if (selection == null)
throw new Error('Text can not be set without a selection.');
return {
...state,
element: setTextElement(text, selection)(state.element),
};
};
}
@mlegenhausen Thank you for the detailed answer. I am using both absurd and io-ts. The problem I have is different. Check the previous comment.
@steida to be honest, I think a library should never throw exceptions. If possible I'd change the signature of setText, to return an Either<string, EditorState>, or These<string, EditorState>. If at the call site you don't know the current state, it could be helpful to return a These to keep track of both error and state.
So something like this:
import * as E from "fp-ts/lib/Either";
export function setText(
text: string
): Endomorphism<E.Either<string, EditorState>> {
return state =>
pipe(
E.fromOption(() => "Text can not be set without a selection.")(
state.selection
),
E.map(selection => ({
...state,
element: setTextElement(text, selection)(state.element)
}))
);
}
Not tested, hopefully I didn't mess up the pipe, my app is still on [email protected] :)
Thank you. I am creating a new contentEditable editor, https://github.com/evolu-io/evolu and I would love leverage fp-ts as much as possible.
Maybe throwing is bad idea, and I should use console.warn for dev instead.
Maybe throwing is bad idea, and I should use console.warn for dev instead.
That's a better behavior IMHO
OK, I got it. Thank for all the answers.
@steida maybe you should reconsider your datamodell. selection is of type Option means all editor elements can at some point be selectable? If not you should move selection to the element (EditorText) itself and remove the Option monad.
Beside this setText looks like a lense and a lense should never fail. Using a lens on a data modell that is in the wrong state should simple return the old state. console.warn could be a solution but is in term of fp-ts it is a side effect.
This is the rest of the model. Selection has anchor and focus paths. They can select el, text by children or text index
export interface EditorSelection {
readonly anchor: EditorPath;
readonly focus: EditorPath;
}
export type EditorPath = number[];
@steida The trick here is to think in a different paradigm
but I would rather tell a developer why the app "does nothing"
Rather than thinking that setText "does something" we should think that _given some value of type EditorState, setText produces another value of the same type EditorState_ and here we should also think about error handling - how should application behave if setText fails. Either application does something with that error, logs, sends to analytics, whatever (means application requires that error), then we should _explicitly_ declare the possibility of failure in setText's type signature (how it is suggested by @pigoz). Otherwise if it's not declared then setText never fails and this means that it handles cases with "invalid" state.
This approach of being explicit in declaring all possible behaviour is essential for functional programming. So, long story short, yeah, __we should never throw__.
Explicitly declare the possibility of failure in setText's type signature (how it is suggested by @pigoz). Otherwise if it's not declared then setText never fails and this means that it handles cases with "invalid" state.
That's one of the main advantages of FP you can see "everything" by looking at the signature of a function. Throwing is so intransparent and has never worked in all the year I wrote OOP code.
Another idea I normally tend when I see this code is to split the concers. It seems more of a "validate and set" function. Maybe you can try to move the validation out by only allowing some sort of state.
interface EditorSelectedState {
state: 'selected'
}
interface EditorUnselectedState {
state: 'unselected'
}
type EditorState = EditorSelectedState |聽EditorUnselectedState
declare function setText(text: string): Endormorphism<EditorSelectedState>
This moves the burden of being in the correct state out of the setter.
maybe you should reconsider your datamodell. selection is of type Option means all editor elements can at some point be selectable? If not you should move selection to the element (EditorText) itself and remove the Option monad.
Yep. It's like DOM Selection, but using EditorPath instead of a reference to DOM Node. Therefore, I don't see any advantage to move a selection to the elements and texts. I hope I am correct.
Btw, it's from this project. I am seriously decided to fix contentEditable once for all, so I really appreciate any help. Thank you all!
https://github.com/evolu-io/evolu/tree/master/packages/evolu/models
@grossbart I know source code of almost all contentEditable editors and have experiences with nasty browser issues and everything is tested through Puppeteer etc, so nothing to laugh about 馃槧馃槅
As I am thinking about it, it seems all EditorState transformations should return Either or These, so when "it does not work" the developer should be allowed to console.log all intermediate errors.
@steida It's a joyous laugh, I think it's really cool that you're so enthusiastic! 馃憦
While I'm at it: I'm in favour of not throwing but returning a value that captures the error, and I support @mlegenhausen's suggestion of using a tagged union for the state, I have found this technique to be very useful.
Maybe I did not get it. How tagged union can help us? Isn't an optional selection good enough? Imagine a document without any selection, until the user click somewhere. How tagged union can help?
This moves the burden of being in the correct state out of the setter
馃憤 Moving the burden to the caller it's rule #1 of FP (or at least among the top 3 馃槀)
@giogonzo But that's what I am doing. It's probably not obvious from the code.
state.selection is Option<EditorSelection>
while
setTextElement requires EditorSelection
Is that what you mean?
export function setText(text: string): Endomorphism<EditorState> {
return state => {
// TODO: Replace toNullable with something.
const selection = toNullable(state.selection);
// https://github.com/gcanti/fp-ts/issues/973
if (selection == null)
throw new Error('Text can not be set without a selection.');
return {
...state,
element: setTextElement(text, selection)(state.element),
};
};
}
Isn't an optional selection good enough?
To call that function you need a selection. This is what I mean by moving the burden to the caller, it will be caller's responsibility to come up with a way to obtain such selection, or not call this function otherwise.
If your domain model is modeled in terms of sum types (as opposed to product types - i.e. the one with optional selection) this becomes clear in the intent from the very beginning. Your function is able to operato only on a specific case (the one with the selection) not the other.
I'm not saying this is the only possible legit solution for the problem here (returning Option/Either encoding the "error condition" could be another valid one, depending on the specific case), but when designing from scratch I tend to prefer it.
I recently saw this talk which explains the concept at a high level (note: there are a gazillion others, and probably better ones, this is just an example): maybe you'll find it useful
Scott Wlaschin is legend. Going to watch him now, thank you!
Oh, I think I got it! The whole setText function needs to have EditorSelectedState, I thought it was a typo 馃槄So the developer has to select something manually before calling setText function.
Thank you all, it was very helpful.
Most helpful comment
Btw, it's from this project. I am seriously decided to fix contentEditable once for all, so I really appreciate any help. Thank you all!
https://github.com/evolu-io/evolu/tree/master/packages/evolu/models