Source code for montle

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import annotations


import contextlib
import math

import functools
import turtle
import enum
import typing

import pint

ureg = pint.UnitRegistry()


# Side Note: Yes, Python has enums !
[docs]class PenState(enum.Enum): UP = -1 DOWN = 1
# nametuple because it is portable, well known, and simple to use. However see http://www.attrs.org/en/stable/why.html#namedtuples # LATER : dataclasses, or frozen attrs.
[docs]class TurtleState(typing.NamedTuple): """ Immutable public State. """ position: turtle.Vec2D = turtle.Vec2D(0, 0) angle: int = 1 * ureg.degrees # pint and types ??? pen: PenState = PenState.DOWN
[docs]class TurtleImpl(typing.NamedTuple): """ Basic class to hold implementation and state together For simplicity sake """ state: TurtleState # Note : if this was a data class we could code here the state extraction from impl. impl: turtle.Turtle
# Ref : https://vimeo.com/162054542
[docs]def schoenfinkel(fun): """ Curry the function : changes some parameters (the ones not passed at first) into a later function call. Effectively splits one call into two. >>> def add(a,b): ... return a + b >>> addfirst = curry(add) >>> andthen = addfirst(2) >>> addthen(3) 5 """ def curried(*args, **kwargs): # TODO : check type signature vs traditional function (haskell and others) p = functools.partial(fun, *args, **kwargs) return p return curried
# TODO : there is probably a more straightforward way fo defining curry from a functools decorator function curry = schoenfinkel
[docs]def uncurry(fun): """ Uncurry the function : changes some :param fun: :return: """ raise NotImplementedError # TODO
class MonadicTurtle: class Lifted: def __init__(self, f): self.f = f def __call__(self, *args, **kwargs): # Note : we define a lifted function for clarity, # but actually python parametric polymorphism is enough to do the job. return self.f(*args, **kwargs) def __init__(self, arg: TurtleImpl): # Note : this is the Type we pass (to start designing monad in a generic way) self.arg = arg def __call__(self, fun) -> MonadicTurtle.Lifted: """ Decorator to mark a function as a generator of M_argtype ie. M_T :param arg: :return: """ # TODO : assert fun is of the right type : one input only, and one output of paramter type of T # TODO : test if function is not partial and has more than one arg -> curry it # TODO : ensure function has this arg, and return it, and generate monad class from it return MonadicTurtle.Lifted(curry(fun))
[docs]@contextlib.contextmanager def montle() -> TurtleImpl: """ A context in which a turtle is available, as well as its state""" ft = turtle.Turtle() yield TurtleImpl(impl=ft, state=TurtleState(position=ft.position(), angle=ft.heading(), pen=ft.pen().get('pendown')))
# TODO : exit cleanly @MonadicTurtle('impl') def move(distance: int, impl: TurtleImpl): # TMP HACK distance = int(distance) # precomputing movable distance to remain in screen # TODO : convert screen coord / worldcoords TL, BL, BR, TR = map(lambda x: turtle.Vec2D(*(impl.impl.screen.screensize()[0] * x[0] * 0.5, impl.impl.screen.screensize()[1] * x[1] * 0.5)), [(-1, 1), (-1, -1),(1,-1),(1,1)]) # forecasting position (TODO : test code) forecast_pos = impl.state.position + (turtle.Vec2D(math.cos(impl.state.angle), math.sin(impl.state.angle)) * distance) #impl.impl.forward(distance) #assert impl.impl.position() == forecast_pos, f"{impl.impl.position()} is not {forecast_pos}" # capping distance (implementing artificial canvas limitations): capped_distance = distance if forecast_pos[0] > (BR + TR)[0] or forecast_pos[1] > (TL + TR)[1]: forecast_pos = ( min( (BR + TR)[0], forecast_pos[0]), min((TL + TR)[1], forecast_pos[1])) if forecast_pos[0] < (BL + TL)[0] or forecast_pos[1] < (BL + BR)[1]: forecast_pos = (max((BL +TL)[0], forecast_pos[0]), max((BL+BR)[1], forecast_pos[1])) capped_distance = math.sqrt( math.pow(forecast_pos[0] - impl.state.position[0],2) + math.pow(forecast_pos[1] - impl.state.position[1], 2)) impl.impl.forward(capped_distance) # moved distance ... # It's the opposite of what we coull be doing here for control... # delta = deltapos(expected_reached, reached_pos) and return delta to adjust next call... # Note : TODO : because of this 04 seems to be a good place to introduce control instead of 3... # TODO : however it is probably better to wait until we get events to log out control and be able to debug/replay it. # TODO : OR NOT ? do we have a way to gradually optimise higher control, ie log of control ? That is, the more it works, the less we want to log about it. # return new state (it is immutable) return TurtleImpl(impl=impl.impl, state=TurtleState(position=impl.impl.position(), angle=impl.impl.heading(), pen=impl.impl.pen().get('pendown'))), capped_distance @MonadicTurtle('impl') def right(angle: int, impl: TurtleImpl): # TMP HACK angle = int(angle * ureg.degrees) impl.impl.right(angle) # return new state (it is immutable) return TurtleImpl(impl=impl.impl, state=TurtleState(position=impl.impl.position(), angle=impl.impl.heading(), pen=impl.impl.pen().get('pendown'))) @MonadicTurtle('impl') def left(angle: int, impl: TurtleImpl): # TMP HACK angle = int(angle) impl.impl.left(angle) # return new state (it is immutable) return TurtleImpl(impl=impl.impl, state=TurtleState(position=impl.impl.position(), angle=impl.impl.heading(), pen=impl.impl.pen().get('pendown'))) @MonadicTurtle('impl') def penup(impl: TurtleImpl): impl.impl.penup() # return new state (it is immutable) return TurtleImpl(impl=impl.impl, state=TurtleState(position=impl.impl.position(), angle=impl.impl.heading(), pen=impl.impl.pen().get('pendown'))) @MonadicTurtle('impl') def pendown(impl: TurtleImpl): impl.impl.pendown() # return new state (it is immutable) return TurtleImpl(impl=impl.impl, state=TurtleState(position=impl.impl.position(), angle=impl.impl.heading(), pen=impl.impl.pen().get('pendown')))