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

Create logical-rules.md #199

Closed
wants to merge 3 commits into from
Closed

Create logical-rules.md #199

wants to merge 3 commits into from

Conversation

mkizub
Copy link

@mkizub mkizub commented Oct 30, 2019

A proposition to add support of logical programming paradigm to Kotlin.
Initial implementation is in my fork of kotlin's compiler

@dector
Copy link

dector commented Oct 30, 2019

Do you think it's possible to create a library for logical programming paradigm? Can you, please, provide some real-life cases when logical programming will be more preferable than functional/oo approach?

@mkizub
Copy link
Author

mkizub commented Oct 31, 2019

Logical programming cannot be added as a library without huge performance impact (each expression shell he wrapped into a closure instance), and syntax will be ugly in any case. Clean syntax for logic conditions is absolutely required for complex logical code.

In my practice I used it (I was writing a compiler with extensions of java)
a) operator overloading with user-defined operators and priorities (full code took 15kb of code), backtracking allowed me to to find all possible ways to parse expressions and report ambiguities
b) analyze type hierarchy and write type inference (it had scala-like complex typesystem) for member access and calls
c) iterate over class members for name completion in IDE
and so on.

I was really missed this feature in my work on others projects, especially to express some complex business logic. But it's hard to provide a small example, since toy tasks can be solved using if/else, and hard logical tasks are hard to explain. Actually, you can imagine a difference by comparing a yacc/bison output with original grammar description. But yacc/bison are DSLs, while logical programming is a general programming paradigm, sutable for manu tasks.

You may see some long examples of code in http://symade.tigris.org project - just checkout and search for 'rule' in code (unfortunately its outdated, compiles only under java 1.6).

PS Expression parser code
http://symade.tigris.org/source/browse/symade/trunk/symade/src/kiev/parser/ASTExprParser.java?revision=126&view=markup

Added an example of a simple logical puzzle solution
@fvasco
Copy link

fvasco commented Nov 1, 2019

Hi @mkizub,
I agree with some of your considerations, however I see some issue in your proposal.

You should explain why Kotlin should support the logical paradigm instead to develop these component in a dedicated language.

Have you considered a Prolog/Clips to bytecode compiler, easily usable by Kotlin code?

Logical programming cannot be added as a library without huge performance impact

Can you link some source?
Have you considered Drools or some other ready to market, full featured tools?

@mkizub
Copy link
Author

mkizub commented Nov 1, 2019

@fvasco, thanks for your questions.

You should explain why Kotlin should support the logical paradigm

It should not :) Kotlin is a language. It has no obligations or wishes.
It's we, programmers, who may wish to add some features to our tools.

instead to develop these component in a dedicated language.

As you know, those languages already exists. In real life the problem is an interoperability
of data and execution models for different languages. The whole point of integrated,
embedded logical engine is that it will use our data directly, it can be used from
Kotlin's code directly and it can use Kotlin's code directly.

Programmers will not need to export program's data and internal API to external
tools. For example, we have a Kotlin compiler that parsed some source code and
built an internal model of that code, with all classes and types. Now we want to
infer a type of expression. How can we use an external Prolog program (compiled
into bytecode) for this task? Export compiler's IR to the form that can be understood
by Prolog and ask it to solve the goal? Even if this logic (of type inference) can
be easily and clearly described in Prolog-like languages, we don't use them.

Can you link some source?

I wrote in parenthesis what kind of performance impact it will have. Creating a
closure instance for each tiny expression is an overkill, IMHO. But this slowdown
is much less important, compared to a clean syntax for complex rules.

I'm currently trying to come with a simple example for this logical engine.
Can you propose one? Currently I'm trying to express a solution for well
known toy puzzle - Wolf, goat and cabbage problem.
Can you take a look at the bottom of logical-rules.md - I spent 10 minutes to solve it using this proposed language extension. It was easy to solve it in Kotlin as well (using sequences and tail recursions). But I spent a few hours (and still not succeeded) to write Kotlin's code to find all solutions.

Can you provide your solution using an imaginary library? Don't bother with implementation,
just write (with some DSL syntax) compilable code.

@fvasco
Copy link

fvasco commented Nov 1, 2019

Hi @mkizub

It should not :) Kotlin is a language. It has no obligations or wishes.

Without strong motivation this KEEP may not be considered, and personally I don't see it.

The whole point of integrated, embedded logical engine is that it will use our data directly, it can be used from Kotlin's code directly and it can use Kotlin's code directly.

Java and Kotlin are different languages and works well togheter.

Can you provide your solution using an imaginary library?

data class State(val man: Boolean, val wolf: Boolean, val goat: Boolean, val cole: Boolean)

fun State.forbidden(): Boolean =
        (wolf != man && wolf == goat) // the wolf eats the goat
                || (goat != man && goat == cole) // the goat eats the cole


val goal = State(true, true, true, true)

fun solve(state: State) = ferry(state).filterNot(State::forbidden)

fun ferry(cs: State) = with(cs) {
    listOfNotNull(
            // the man can
            // cross alone
            copy(man = !man),
            // ferry the wolf
            copy(man = !man, wolf = !wolf).takeIf { man == wolf },
            // ferry the goat
            copy(man = !man, goat = !goat).takeIf { man == goat },
            // ferry the cole
            copy(man = !man, cole = !cole).takeIf { man == cole }
    )
}

fun main() {
    process(setOf(State(false, false, false, false)), ::solve)
            .filter { it.last() == goal }.take(20)
            .forEach(::println)
}

// library

fun <T : Any> process(initialFacts: Set<T>, infer: (T) -> Iterable<T>) = sequence {
    val facts = HashSet<List<T>>()
    var agenda = initialFacts.map { listOf(it) }.toSet()
    while (agenda.isNotEmpty()) {
        yieldAll(agenda)
        facts += agenda
        agenda = agenda.flatMap { previous -> infer(previous.last()).map { previous + it } }.toSet()
    }
}

@mkizub
Copy link
Author

mkizub commented Nov 1, 2019

Hi @fvasco
Thank you for a good example of functional approach to solve the puzzle.
It also demonstrates the main difference of logical paradigms.
Your solution collects all possible states of the system, and filters them, until the goal state will be found. Your code, also, does not track the sequence of river crosses, it just reports that the goal state is reachable. But this can be fixed, of cause.

And as you see, the difference is that logical code makes depth-first traverse of the graph of states, while your solution makes breadth-first traverse. That's because the distinguishing characteristic of logical programming is the possibility to backtrack to previous state, and try another side of decision fork. And depth-first lookup is natural for logical programming.

Definitely none of approaches is better for all situations. The motivation for adding logical paradigm to Kotlin is the same as motivation of adding elements of functional or object-oriented programming. It allows to solve many programming tasks in a cleaner and safer way, compared to imperative or functional approaches.

Java and Kotlin are different languages and works well togheter.

Well, Kotlin is the same language for all JVM/JS/Native platforms, but they don't work together well. Language/platform compatibility is a very subtle subject. Otherwise, why they added closures and other functional features to Java, if that many functional languages for JVM exists? Why not use Eta (Haskell for JVM) or Kawa (Scheme for JVM), right?

@fvasco
Copy link

fvasco commented Nov 2, 2019

Hi @mkizub

It also demonstrates the main difference of logical paradigms.

It answers to your direct question. It shows that your example does not prove that this KEEP helps in Kotlin development.
As @dector requested, you should "provide some real-life cases when logical programming will be more preferable than functional/oo approach".

Your solution collects all possible states of the system, and filters them, until the goal state will be found.

No, it does not.
My solution collects all possible states of the system. Stop.
Printing only a particular one is an external behaviour, the inferencing proves all facts without a goal. Providing a goal is not a requirement.

And as you see, the difference is that logical code makes depth-first traverse of the graph of states, while your solution makes breadth-first traverse. That's because the distinguishing characteristic of logical programming is the possibility to backtrack to previous state, and try another side of decision fork. And depth-first lookup is natural for logical programming.

Are you joking?
There is not requirements of backward or forward chaining in logical programming, only facts and logical rules. (where facts are optional, btw :)

I think you should start from the How to propose paragraph.

@mkizub
Copy link
Author

mkizub commented Nov 2, 2019

Hi @fvasco
I feel this conversation to be like two workers discuss their tools.
One proposes to another to use an ax, stating it's a very helpful tool.
The second worker says that he's not convinced, that an ax is a good tool, and asks the first one to give some examples. And comments, that in this case a knife can be used, in this case a hammer can be used and so on.

Proposed logical engine is a tool for programmers. It has many useful features, for example
a) the same code can be used to find a solution, to find all solutions iteratively, to check if a solution is valid;
b) it can be used like a (stateful) generator;
c) it can be used like a co-routine (that yields each time it has found a solution, and can be re-entered to find next one);
d) it can be used as a compact, clean and efficient replacement for all those chained filter/map/etc;
and so on.

You say, that one can use sequences, co-routines, functional programming etc.
Yes, I agree with you. In the same way as the first worker must agree with the second, that a hammer, a knife, a scraper can be used instead of the ax. If the second worker never used an ax, or his tasks were very different (a watchmaker really does not need an ax), how he can be convinced that it's a good tool?

I cannot convince you in the same way. I was using this logical engine, and found it to be very useful. That's all I can say. It does not convince you? That's fine. I have no idea about your background, maybe you are like a watchmaker, never had a need of ax. I asked you whether you can propose an example task. A one that cannot be easily solved in functional or imperative paradigm. If you don't have one, than you, probably, will not need anything beyond these paradigms.

I argue that adding this logical engine to Kotlin is a simple task. I did it withing last 2 weeks, and 90% of time was spent to learn internals of the compiler. It can be production-ready in a month, if someone will sit near me and answer my questions about the compiler and idea plugin.

I argue that this logical engine is compatible with Kolin. Because very little changes are needed for it's syntax and nothing in semantic. Rules are just functions that return iterators. Period.

And for this scanty price Kotlin will get support of the logical paradigm. Fully integrated. Being the first widely used language that has support of all imperative, objective, functional and logical paradigms (well, maybe second after Lisp, but Lisp can everything by definition :) ).

@fvasco
Copy link

fvasco commented Nov 3, 2019

Hi @mkizub,
I have to think to be expressed very bad my concerns.

I like logical programming and I think that your idea may improve the language.
However, regardless of my background, I friendly suggest you to prove your statements and show how much improvement this idea can bring to Kotlin.

Honestly, I don't think that an answer like «It should not :) Kotlin is a language. It has no obligations or wishes.» is a good step in the right direction.

Your proposed puzzle is a good idea, unfortunately your solution, compared to a pure Kotlin one, requires the trick:

!seq.contains(-ns), // prevent infinit loop
// add new state, and remove it on backtracking
seq.push(-ns) ?< seq.pop()

Maybe you should evaluate this suggestion to evolve this KEEP and/or to search a better example.

Finally I want to highlight you that some slogan like «it can be used as a compact, clean and efficient replacement for all those chained filter/map/etc; and so on.» are really interesting, but you should show how much it is compact, you should explain why it is cleaner and you should prove how much it is efficient. Unfortunately these advantages are not evident to me.

Without data, facts and good example, this KEEP does not look valuable to me.
I repeat: this KEEP looks as a good idea badly sold.

But this is only a personal opinion, and my value is zero in this repository. Good luck!

@fvasco
Copy link

fvasco commented Nov 5, 2019

Hi @mkizub

Finally I've come to idea to write you directly, without polluting the KEEP discussion.

Please avoid direct mail, it is hard to track and inaccessible to other contributors, you can find really good suggestions in the -already mentioned- "How to propose" section.

Please, express the example (with the farmer and goat/wolf/cabbage) or any other program in your syntax. So we can see how much of expressiveness we will loose using a library, instead of proposed syntax.

I solved the quiz using valid Kotlin syntax, I don't see any reason to design a different syntax to solve an already solved task.

Finding ALL solutions for the puzzle is not that easy.

I fixed the example, I have to consider that the difficulty is really subjective.

Obviously, the farmer may load the goat and carry it forth and back 100 times, before trying to ferry the wolf or the cabbage

I agree, however the "trick" is an implementation detail.

It's not possible to explain "why it should" if it should not.

This is the Occam Preferred Solution™ and it looks really reasonable to me.

Please avoid to submit to me further requests, I already expressed my -personal- point of view and each point of this discussion does not carry any valuable thought.
I am not able to esteem the preciousness of this KEEP.

@mkizub
Copy link
Author

mkizub commented Nov 5, 2019

Hello @fvasco
Thank you for the fixed solution, so it can be directly compared to the solution provided in this pull request. Now peoples can compare your code and mine and decide which one is simpler to understand.

Sorry for the direct e-mail, I had no intention to be intrusive. But I explained my reasons to send it, and they are still working, that's why I will not make a detailed response to your answer.

@quickstep24
Copy link

Personally, I would prefer a DSL syntax within regular Kotlin over a new "logic" syntax (expressiveness over conciseness). I believe this does not have to be ugly.
The performance benefits of a language extension over a library solution have to be proven based on real benchmarks. Do you have values for comparison?
A library solution has the advantage that the library can be exchanged (or at least some part of it) when necessary, for example for distributed problem solving, dedicated memory management, etc.
Compiled version has to be one size fits all.

Concerning your example:
The man can only ferry items which are on his side of the river.
I think your rules allow to transition from man=true, wolf=false to man=false, wolf=true.

@mkizub
Copy link
Author

mkizub commented Nov 6, 2019

@quickstep24 , thanks for the found bug in the sample puzzle, I fixed it. Of cause, it should be like

// the man can
    {   // cross alone
        ns ?= cs.copy(man = !cs.man)
    ;   // ferry the wolf
        cs.man == cs.wolf,
        ns ?= cs.copy(man = !cs.man, wolf = !cs.wolf)
    ;   // ferry the goat
        cs.man == cs.goat,
        ns ?= cs.copy(man = !cs.man, goat = !cs.goat)
    ;   // ferry the cole
        cs.man == cs.cole,
        ns ?= cs.copy(man = !cs.man, cole = !cs.cole)
    },

As for the library/DSL. It took me about 4 month to invent such a logical engine. Believe me or not, I started from an attempt to write a library. Well, actually I started with attempts to use existing Prolog interpreters (libraries) for JVM. But It was impossible to export whole program's data to those libraries, so I evolved to an attempt to write a library, where methods were implementing all those AND/OR/CUT/etc logical primitives. But this effectively means that every expression in logical code will be a lambda wrapped into a closure. A separate inner class for each expression. Imagine your program, it's size and performance, if every statement/expression in it will be compiled into a separate inner class.

Do you have values for comparison?

No. Sorry, the idea of wrapping every expression into an inner class made such a lasting impression, that I not even tried this approach. I think, the performance will not be significantly down - those closures (for each expression) will be created on method entry, and after that an execution of each expression will be wrapped into a call. Maybe 2 or 3 times slower?.. Not a big deal.

What is more important is the syntax and semantic. I tried to invent a good syntax many times, but not found a good one. Words and and or are used too often in the rest of code, to reserve them for rules. Unneeded {..} around each expression. Hard to indent code. And so on.

As for semantic, all those AND/OR/CUT/IF-THEN/WHILE/WHEN rules are flow controls, not just expressions. Flow controls may be analyzed and optimized. For example, code from the puzzle

    ! (-ns).forbidden(),
    !seq.contains(-ns),

shell be optimized into one boolean expression

    ! (-ns).forbidden() && !seq.contains(-ns)

during code optimization.

Also, the compiler may track bound and unbound states of logical variables. Actually, it's very like tracking for nullable values. Because unbound logical variable has no value, it's null actually. Otherwise we will get all those problems that nullable values were causing without accurate flow control analyze. Maybe, with new Kotlin's conditions engine it will be possible in future, but now it's too limited.

@mkizub mkizub closed this by deleting the head repository May 22, 2024
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.

None yet

4 participants