Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add Event3 and Event4 #1

Closed
wants to merge 1 commit into from

Conversation

mizunashi-mana
Copy link

I wanted.

@kimamula
Copy link
Owner

Well, basically I think you'd better pass an argument as an object if your event requires more than two parameters, e.g.

event(event: 'saveUser'): Event1<MyEventEmitter, {
    id: number;
    name: string;
    age: number;
}>;

is much more readable than

event(event: 'saveUser'): Event3<MyEventEmitter, number, string, number>;

Thus I believe limitting the number of arguments as much as two is reasonable.

Is there any special use case in which you want to pass three or four arguments separately rather than passing them as properties of a single object?

@mizunashi-mana
Copy link
Author

Oh, thx wonderful solution. I had no idea such as.
However, this solution have a problem.

This case is invalid:

event('saveUser').emit({id: 0, name: 'string'}); # throw error

But, this case is valid:

event('saveUser').emit({id: 0, name: 'string', age: 0, extra: 'extra'});

The type of { id: number; name: string; age: number; } is interface. Typescript interface is structural subtyping. That is the reason.
(In more cases, structural subtyping is useful, sometimes not useful :( )

So, I think the way is not better way.

@kimamula
Copy link
Owner

Could you make clear why you think structural subtyping is not useful in some cases?
TypeScript helps us avoid runtime erros arising from type mismatch by detecting them as compilation errors.
Obviously,

event('saveUser').emit({id: 0, name: 'string', age: 0, extra: 'extra'});

does not raise a runtime error, thus it's natural that TypeScript compiles the code without errors.

@mizunashi-mana
Copy link
Author

Hmm...?

Frankly, I want to throw error in this case:

event('saveUser').emit({id: 0, name: 'string', age: 0, extra: 'extra'});

Because arguments is over supply. If we use optional parameter, using structural subtyping (instead ?).
But, if we use certain arguments over three, I think we will use Event3 or Event4.
Don't we?

@kimamula
Copy link
Owner

Do you mean you always do not want to use values of non-primitive types as function arguments if there is no optional argument, or you are discussing about some special cases?

If the former is the case, you have to make most of your functions accept only primitive types, which will prevent you from using many useful design patterns as well as an effective object-oriented programming, I'm afraid.

If the latter is the case, please provide an example.

@mizunashi-mana
Copy link
Author

interface MyEventEmitter extends TsEventEmitter {
  event(name: 'rollback'): Event4<MyEventEmitter,
    Error, // error
    string, // message
    enum, // my error type
    Function>; // callback
}

const myEmitter : MyEventEmitter = TsEventEmitter.create();

test.except(myEmitter.event('rollback').on((error, msg, errtype, fn) => {}).to.be.ok();
test.except(myEmitter.event('rollback').on((error, msg, errtype) => {}).to.throw(Error);
test.except(myEmitter.event('rollback').on((error, msg, errtype, fn, true) => {}).to.throw(Error);
test.except(myEmitter.event('rollback').on((error, msg, errtype, true) => {}).to.throw(Error);

The case, force to receive and catch all of parameters by rollback function.
In the case, limit parameters to send on event's emit function, so we except not to exist own error information on every emit function caller.
How is this?

@kimamula
Copy link
Owner

Sorry, I cannot understand what you mean in this sentence.

In the case, limit parameters to send on event's emit function, so we except not to exist own error information on every emit function caller.

@mizunashi-mana
Copy link
Author

Hmm...

function ... {
  ...
  myEmitter.event('rollback').emit(error, msg, errtype, fn, true)
  ...
}

Last argument, true maybe mean a flag of escaping. But, event listener is not received, because myEmitter.event('rollback')'s function is received 4 args. Maybe this is careless miss.
Event4 will be guarded.

However, in this case of using event('saveUser').emit({id: 0, name: 'string', age: 0, extra: 'extra'});
Last argument, extra is through type checker.

Actually, there are some situations excepting through. But, there are not excepting situations, too.

@kimamula
Copy link
Owner

Well, why do you have to worry about someone might expect an escape property of the argument to affect the behavior of the following function, which sounds like a kind of obsession rather than a careless mistake to me?

rollback({
  error: Error;
  message: string;
  errorType:  ErrorTypeEnum;
  callback: Function
}): void {
  ...
}

This is not a matter of structural subtyping.
As in Java, for example,

interface MyInterface {
  String stringProperty;
}

class MyClass {
  public void doSomething(MyInterface arg) {
    ...
  }
}

you cannot avoid someone passing an object which implements MyInterface but has extra properties other than stringProperty.

However, you can define Event3 and Event4 in your code and use it as you like.
I'd like this library to keep as simple as possible.

@mizunashi-mana
Copy link
Author

Hmm...

Maybe the reason why I cannot understand your mind is my few knowledge about Typescript and English.

Interface is structural subtyping.
So, In the case I use object type literal or interface to define type by Generics (e.g. MyClass<{ a: number }>), I allow to call with all of types having the interface properties (e.g. classes implements { a: number } or object having property a the type is number).
And, In the case I use Event3, I allow to call with only 3 type arguments defined me.
Neither more nor less.

you cannot avoid someone passing an object which implements MyInterface but has extra properties other than stringProperty.

Sorry for my explaining, I explained with concrete examples on prev comment for helping your understanding. However, it may provide more difficult.
I don't say I don't allow to specify Object on Event1's argument type.

I'd like this library to keep as simple as possible.

I intend to understand this thinking. And, I think this classes are needed for this library.
Actually, Event4 maybe be not needed. But, I think of Event3 needed.

As a reference, C# (which are program languages I use usually) define for 8 less arguments.
(refs)

And, maybe haskell have been allowed 7.
(refs)

And, there is ternary operations and using some program languages. So, I think proper for Event3 at least.

@kimamula
Copy link
Owner

Actually, TypeScript provides tuple whose length is not limited. So you can define your event as follows.

event(name: 'myEvent'): Event1<MyEventEmitter, [number, string, boolean]>;

However, this would not fill your requirement because TypeScript tuple accepts an array whose length is longer than defined.

I'm not familiar with C# nor Haskell, but I am with Scala, in which maximum length of tuple is 22.
I think tuple is very convenient data structure, but the use of it (especially long one) should be limited in some use cases.
As an example, let me show you how Play framework, which is the most famous web framework of Scala, uses tuple to covert parameters send by the form to Scala object (and vice versa).

val userForm = Form(
  mapping( // defining the form data structure
    "id" -> number,
    "name" -> text,
    "age" -> number
  )( 
    (id, name, age) => User(id, name, age), // convert form parameters (as a tuple) to Scala object
    (user: User) => Some(user.id, user.name, user.age) // convert Scala object to values (tuple) to be assigned to the form
  )
)

In this example, the provider and the consumer of a tuple exists very close, and it is quite easy to confirm that each element of the tuple is used as expected.

On the other hand, when you use EventEmitter, the dispatchers and the listeners of the event are loosely-coupled.

  • The dispatchers do not know which object is listening to the event and how the listener treats the event payload.
  • Likewise, the listeners do not know which object dispatches the event and how the dispatcher creates the event payload.

In this situation, the definition of the event payload should be as declarative as possible, thus

event(event: 'saveUser'): Event1<MyEventEmitter, {
    id: number;
    name: string;
    age: number;
}>;

is much better than

event(event: 'saveUser'): Event3<MyEventEmitter,
    number, // id
    string, // name
    number // age
>;

If you use the latter definition, some event dispatcher may assign body weight to the third argument, and some event listener may treat the first argument as age and the third argument as id (comment can be easily ignored sometimes, as a result of "careless mistakes").

@kimamula
Copy link
Owner

It's good to know you are a C# person.
I would like to ask you if you also worry about same thing "in some cases" when you are writing C#.

interface MyInterface
{
  String stringProperty;
}

class MyClass
{
  public void doSomething(MyInterface arg)
  {
    // you cannot prevent an object which is an instance of MyInterface but has a property other than stringProperty
    // to be passed as an argument of this method
  }
}

@kimamula
Copy link
Owner

kimamula commented Sep 6, 2015

TypeScript 1.6.0-beta provides with strict object literal assignment checking functionality, which aims to avoid typo of the keys of optional parameters but would also meet your requirements.
So I close this PR.

@kimamula kimamula closed this Sep 6, 2015
@mizunashi-mana
Copy link
Author

Oh, sorry. I have no time recently.
Thx for many advices. They are very instructive for me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants