Asynchronicity
Sometimes synchronicity is not enough
Sometimes synchronicity is not enough
Jan 22nd
I’ve been using emacs as my main editor for a good number of years now, starting when I had to use and old version of Linux for a project. At the time there wasn’t much real choice for a programmer’s editor in Linux, beyond the usual: vi and emacs. I’ve never liked vi’s modal interface (got fed up with all the beeping while trying to use it), so I went with emacs. It’s decidedly old-school, and it’s not perfect, but few other editors have what I need from it: features, easy portability and easy extendability. Among the newer editors I’ve tried (a lot of them) the only one that comes close to emacs is Sublime Text 2.
The ability to easily reprogram or extend your programming tools, especially your editor, is important and often underrated. After some time doing Java professionally, I’ve dabbled in extending Eclipse, even implementing a simple plugin to support OCaml. It’s an environment where the simplest extension or modification is a nightmare to do. Eclipse: write a lot of XML files, select classes to subclass, understand the design patterns and complex relationships between the classes you’re subclassing, write a lot of boilerplate code, compile, open a new instance of Eclipse, test, repeat. Emacs: write some lines of emacs lisp, eval, test, repeat. So much for OO. Other IDEs like Visual Studio aren’t much better than Eclipse, as far as I know.
Of course, this all comes at the cost of a steep learning curve. And to be able to effectively extend Emacs you have to learn Lisp. However, in both cases I think they are worthy investments for a programmer’s toolbelt. Not everyone has to like and use emacs, but a programmer should learn to Use a Single Editor Well, in the words of the pragmatic programmers. Do you truly command your editor, or do you only know how to type, copy & paste?
Aug 1st
So I was reading this post by Rafael on his experience with Ruby and started writing a comment, mostly about the typing issue. I soon realized that there was too much I wanted to say in a comment, hence this post here.
I believe I have a good amount of experience both with static typing and dynamic typing (for simplicity, I’ll be sloppy with the terms here, for instance calling it “dynamic typing”, even though it may be more precise to talk about “dynamically-typed languages” or “unityped languages”). For a lot of time I programmed in C and C++ (and some Java), then I wrote a lot of Scheme and Common Lisp while learning about Lisp and functional programming, and later I discovered the statically-typed functional languages like OCaml and Haskell. The last two are my preferred languages currently, so I’d say I have a preference for static typing, or at least for more “modern” static typing systems. Between Java/C++ and Lisp, I think I’d prefer Lisp.
But although I have studied some programming language theory, mostly from the typed side, I don’t at all agree with the “statically-typed fundamentalist” view that dynamic typing is just a special case of static typing (like argued here by Robert Harper), and therefore you have nothing to gain by using a dynamically-typed language. The theoretical argument may be correct, but correctness does not translate to convenience in a real programming situation. I do think dynamic typing has its advantages, mostly for exploratory programming and prototyping.
Of course, if I still prefer to program in statically-typed languages, I must have reasons for it, and indeed I do. One of these is that types are a form of documentation: when writing a Lisp function I often put the typing constraints the function expects in a comment, so that callers know what to pass to it. Why not make these annotations verifiable by the compiler? This is also the reason many Haskell programmers write function types explicitly, even when the compiler can infer them most of the time. Having type inference is very good for rapid prototyping and to avoid excessive verbosity whenever needed. Also, even if I don’t write explicitly the types for a function in a language with type inference, a good programming environment can show the inferred types, thus fulfilling the documentation role of types in a way that’s not easy to do with dynamic typing.
Regarding tests, it’s true that tests can catch typing errors, and that type-checking by the compiler will catch only a fraction of the program’s bugs. If your specification is wrong, no typing system will help you, ever, unless it uses advanced artificial intelligence and, in this case, you’re not programming anymore because the machine is doing everything. However, saying only that tests can catch typing errors as well as the compiler for a static language misses one important point: the compiler does it automatically, while you, the programmer, have to write the tests. Using tests to catch type errors shifts the burden from the compiler to the programmer. Because type-checking can’t catch all program errors, it’s still necessary to write tests even in a static language, but the amount of tests you’d have to write is smaller as you don’t need to check for simple type constraints.
Statically-typed languages also tend to have better performance and better support for programming tools. I don’t believe this is inherent to static typing, but it seems to be easier to write better compilers and better tools if more analyses can be done with certainty because the language is designed to keep specific typing guarantees. Types also allow for nice techniques like typed compilation and typed intermediate languages, which I think could be better explored by compiler and tool writers.
All in all, programming in a modern statically-typed language with type inference gives me most of the convenience of dynamically-typed languages, with all the advantages of a static type system and associated compiler. There are some times when I miss the freedom of dynamic typing when programming in a static language, but so there are times when I miss the conveniences of type-checking when programming in a dynamic language.
I guess the solution is to integrate the best aspects of both sides, and this seems to be a research theme for many important people in the area of programming languages recently (examples: Visual Basic, Typed Racket, and the Types Considered Harmful presentation by Pierce). At least we can hope this holy war will, one day, be settled for all time.
Jul 16th
No post anterior foi mostrado como resolver desafios de lógica no estilo do Teste de Einstein em Prolog. Entretanto, o processo de geração no esquema de geração e teste era bastante ineficiente para problemas mais complexos. O problema é que muitas possibilidades são geradas e descartadas quando é feita a verificação de diferença. Por que não gerar as possibilidades já todas diferentes, eliminando esse teste extra?
Em Prolog o predicado select(X, L, R) seleciona o elemento X na lista L, e retira X de L formando o resto R. Ou seja, R tem todos os elementos de L, menos o elemento selecionado X. Isso pode ser usado para, dada a lista de possibilidades para cada atributo (por exemplo, cores de casa), retirar uma possibilidade da lista para gerar uma casa, depois usar o resto da lista para gerar as cores para as outras casas. Assim, duas casas nunca serão geradas com a mesma cor.
Como exemplo, vamos usar o próprio teste de Einstein, porque ele tem mais possibilidades e não é possível de resolver eficientemente com o esquema do post anterior. O problema tem cinco casas, cada uma com cinco atributos: a cor da casa, a nacionalidade do morador, a bebida preferida do morador, o cigarro preferido e o animal de estimação. As dicas são:
Das dicas podemos tirar as possibilidades de cada atributo. Para gerar as cores, usamos o predicado abaixo, que é só um wrapper mais conveniente para select, passando a lista de cores disponíveis e recebendo o resto das cores disponíveis na última posição:
gera_cor(casa(C, _, _, _, _), [C], []) :- !.gera_cor(casa(C, _, _, _, _), Cores, Resto) :- select(C, Cores, Resto).
Depois é só gerar os outros atributos da mesma forma:
gera_nac(casa(_, N, _, _, _), [N], []) :- !.gera_nac(casa(_, N, _, _, _), Nacs, Resto) :- select(N, Nacs, Resto).
gera_beb(casa(_, _, B, _, _), [B], []) :- !.gera_beb(casa(_, _, B, _, _), Bebs, Resto) :- select(B, Bebs, Resto).
gera_cig(casa(_, _, _, C, _), [C], []) :- !.gera_cig(casa(_, _, _, C, _), Cigs, Resto) :- select(C, Cigs, Resto).
gera_ani(casa(_, _, _, _, A), [A], []) :- !.gera_ani(casa(_, _, _, _, A), Anis, Resto) :- select(A, Anis, Resto).
Com isso, podemos gerar uma casa inteira e um conjunto de casas usando um mapeamento recursivo:
gera_casa(C, atr(Cs, Ns, Bs, Cigs, As), atr(Cs2, Ns2, Bs2, Cigs2, As2)) :- gera_cor(C, Cs, Cs2), gera_nac(C, Ns, Ns2), gera_beb(C, Bs, Bs2), gera_cig(C, Cigs, Cigs2), gera_ani(C, As, As2).
gera_casas([], _) :- !.gera_casas([C|Cs], Atribs) :- gera_casa(C, Atribs, Atribs2), gera_casas(Cs, Atribs2).
gera_sol([C1, C2, C3, C4, C5]) :- Cores = [amarela,azul,branca,verde,vermelha], Nacs = [alemao,dinamarques,ingles,noruegues,sueco], Bebs = [agua,cafe,cerveja,cha,leite], Cigs = [blends,bluemaster,dunhill,pallmall,prince], Anis = [cachorro,cavalo,gato,passaro,peixe], gera_casas([C1, C2, C3, C4, C5], atr(Cores, Nacs, Bebs, Cigs, Anis)).
A estrutura atr guarda as listas com todos os atributos disponíveis, para simplificar o código.
Para resolver o teste de Einstein é preciso estabelecer quando dois moradores são vizinhos, como é mencionado em várias dicas. Considerando a lista de soluções S, como gerada pelo predicado gera_sol, acima, podemos criar predicados simples que testam (ou geram) moradores vizinhos na solução, inclusive separando vizinhos esquerdos de vizinhos direitos, pois uma dica especifica o lado:
vizinho_esq(C1, C2, [C1,C2|_]).vizinho_esq(C1, C2, [C3|T]) :- vizinho_esq(C1, C2, T).
vizinho_dir(C1, C2, [C2,C1|_]).vizinho_dir(C1, C2, [C3|T]) :- vizinho_dir(C1, C2, T).
vizinho(C1, C2, S) :- vizinho_esq(C1, C2, S).vizinho(C1, C2, S) :- vizinho_dir(C1, C2, S).
Agora é só traduzir as dicas e fazer os testes para obter a solução. Aqui mais um truque pode ser usado: embora a ordem conceitual seja gerar as possibilidades e depois testa-las, no caso de muitas possibilidades (como neste desafio) o processo computacional pode se tornar ineficiente. Então usamos as propriedades do Prolog para resolver o desafio mais rapidamente. Em um predicado Prolog “puro” (sem cortes ou negação) podemos mudar a ordem dos objetivos em uma conjunção sem alterar o resultado. Todos os predicados usados até agora são puros, então podemos colocar os testes antes, para criar restrições sobre a solução, e chamar o predicado de geração depois para verificar as restrições e gerar o que falta. Isso feito, o resultado para o teste de Einstein sai bem rápido em um computador atual. Com essas considerações, o código que testa as dicas do problema e encontra a solução é:
solucao(S):- C1=casa(_,noruegues,_,_,_), C3=casa(_,_,leite,_,_), S=[C1,C2,C3,C4,C5], !, vizinho_esq(casa(verde,_,_,_,_),casa(branca,_,_,_,_), S), vizinho(casa(_,noruegues,_,_,_), casa(azul,_,_,_,_), S), vizinho(casa(_,_,_,blends,_),casa(_,_,_,_,gato), S), vizinho(casa(_,_,_,_,cavalo),casa(_,_,_,dunhill,_), S), vizinho(casa(_,_,_,blends,_),casa(_,_,agua,_,_), S), member(casa(vermelha,ingles,_,_,_), S), member(casa(_,sueco,_,_,cachorro), S), member(casa(_,dinamarques,cha,_,_), S), member(casa(verde,_,cafe,_,_), S), member(casa(_, _, _, pallmall, passaro), S), member(casa(amarela,_,_,dunhill,_), S), member(casa(_,_,cerveja,bluemaster,_), S), member(casa(_,alemao,_,prince,_), S), gera_sol(S).E o resultado sai realmente rápido:
?- solucao(S).
S = [casa(amarela, noruegues, agua, dunhill, gato),
casa(azul, dinamarques, cha, blends, cavalo),
casa(vermelha, ingles, leite, pallmall, passaro),
casa(verde, alemao, cafe, prince, peixe),
casa(branca, sueco, cerveja, bluemaster, cachorro)] .
O programa completo pode ser visto aqui. A elaboração deste programa contou com a colaboração do aluno Josué Gomes (@jgomesj).
Jun 28th
Dado que em Prolog é fácil criar estruturas (termos compostos) e fazer buscas sujeitas a certas restrições, é fácil resolver os tais desafios de lógica usando a linguagem. O tipo de desafio de lógica em questão é similar aquele “teste de Einstein” que algumas correntes de email dizem que só meia-dúzia de pessoas no mundo conseguem resolver (o que é uma grande balela). Várias informações (mas não todas) são dadas sobre um conjunto de indivíduos, e é preciso deduzir as informações que faltam para chegar na solução. Resolver um problema desses em Prolog é uma forma de explorar várias características de linguagem e alguns padrões de uso.
A Coquetel publica uma linha de revistas de Desafios de Lógica desse tipo, usando uma tabela para ajudar a cruzar as informações e resolver o problema. Esses problemas são um pouco diferentes porque algumas informações não estão nas dicas, mas apenas nas listas de atributos nas tabelas, e porque às vezes é preciso raciocinar por exclusão (assumindo que não existem duas pessoas com o mesmo atributo). Para a solução (em Prolog) de um puzzle simples no estilo do teste de Einstein, veja esse post acolá. Aqui vou mostrar a solução para um problema de uma revista Desafios de Lógica (número 98). Sem colocar a tabela, aqui estão as informações dadas (espero que a Coquetel não me processe):
Desafio: Primeiro de Abril
Quatro pessoas que gostam de pregar peças decidiram tornar o primeirode abril inesquecível, ou seja, com muitas brincadeiras. Cada um pregou uma peça numa vítima diferente usando um objeto inofensivo.
- Nomes: Ana, Ester, Pablo, Rodolfo
- Sobrenomes: Fontes, Levis, Matoso, Salgado
- Brincadeiras: Almofada de barulho, Aranha falsa, Foto alterada, Mosca falsa
- Vítimas: Irmão, Mãe, Pai, Tia
Dicas:
- Ana deu risadas enquanto colocava uma aranha falsa na comida de sua vítima.
- A pessoa de sobrenome Salgado (que não é Ana) pregou uma peça em seu irmão.
- A pessoa de sobrenome Matoso colocou uma almofada de barulho na cadeira de sua vítima.
- Rodolfo pregou uma peça em sua tia, mas não foi ele que usou a almofada de barulho.
- A brincadeira feita por Levis incluía uma mosca falsa. A vítima de Ester foi seu pai.
Para garantir que todas as possibilidades são examinadas e são testadas posteriormente, incluindo as partes de dedução por exclusão, decidi usar o padrão “gerar e testar” (generate-and-test) que é comum em Prolog: gerar todas as possibilidades e ir restringindo (testando) até achar a solução.
Começando com a geração: o procedimento a seguir gera todas as configurações possíveis, se for usado backtracking. Para representar as informações de uma pessoa, usei uma estrutura p(Nome, Sobrenome, Brincadeira, Vítima).
/* p(Nome, Sobrenome, Brincadeira, Vitima) */
gera(p(N, S, B, V)) :-
member(N, [ana, ester, pablo, rodolfo]),
member(S, [fontes, levis, matoso, salgado]),
member(B, [almofada, aranha, foto, mosca]),
member(V, [irmao, mae, pai, tia]).
O procedimento gera as possibilidades corretas usando o predicado pré-definido member, garantindo que o nome gerado será um entre [ana, ester, pablo, rodolfo], e por aí vai. Podemos testar a geração fazendo uma consulta como
?- gera(P).
O próximo passo é gerar não só um indivíduo, mas uma solução completa, que são quatro indivíduos. Se simplesmente usarmos
?- gera(P1), gera(P2), gera(P3), gera(P4).
serão gerados muitos grupos com indivíduos repetidos. Precisamos garantir a geração de quatro indivíduos diferentes em todos os atributos. Não funciona se tentarmos usar P1 \= P2 e por aí vai, porque o Prolog vai dizer que duas estruturas que diferem em apenas um dos termos são diferentes. Por exemplo,
?- p(ana, fontes, almofada, irmao) \=
p(ana, fontes, almofada, mosca).
true.
Precisamos diferenciar todos os atributos. Além disso, é preciso ter quatro indivíduos diferentes dois a dois, pois se P1 é diferente de P2 e P2 é diferente de P3, é possível que P1 seja igual a P3, e por aí vai. Assim, definimos
dif(p(N1, S1, B1, V1), p(N2, S2, B2, V2)) :-
N1 \= N2, S1 \= S2, B1 \= B2, V1 \= V2.
todas_dif(P1, P2, P3, P4) :-
dif(P1, P2), dif(P1, P3), dif(P1, P4),
dif(P2, P3), dif(P2, P4), dif(P3, P4).
Agora, se testarmos uma consulta do tipo
?- gera(P1), gera(P2), gera(P3), gera(P4), todas_dif(P1, P2, P3, P4).
Só veremos soluções em que as quatro pessoas são diferentes em todos os atributos. Falta testar as possibilidades geradas com as dicas dadas no problema para encontrar a solução.
A solução consiste de quatro indivíduos, todos diferentes (usando o critério de diferença discutido), que se encaixem nas dicas dadas pelo problema. A ideia é capturada no procedimento que resolve o problema:
solucao(S) :-
S = [P1, P2, P3, P4],
gera(P1), gera(P2), gera(P3), gera(P4),
todas_dif(P1, P2, P3, P4),
member(p(ana, _, aranha, _), S),
member(p(N, salgado, _, irmao), S), N \= ana,
member(p(_, matoso, almofada, _), S),
member(p(rodolfo, _, B, tia), S), B \= almofada,
member(p(_, levis, mosca, _), S),
member(p(ester, _, _, pai), S).
O primeiro objetivo da solução apenas diz que S (a solução) é uma lista de tamanho 4, formada por P1, P2, P3 e P4. Na segunda e terceira linha do corpo da cláusula, temos o padrão de geração discutido antes. Depois, cada linha é um teste que representa uma dica do problema. O primeiro teste diz que Ana usou a Aranha como brincadeira, mas não conhecemos seu sobrenome nem qual foi sua vítima; esse indivíduo faz parte da solução (usando o predicado pré-definido member). A segunda diz que a pessoa de sobrenome Salgado pregou uma peça no irmão; não conhecemos o nome dessa pessoa, mas sabemos que não é Ana, como expresso pela desigualdade seguinte. As outras linhas traduzem as outras dicas, sem maiores novidades.
Carregando o código inteiro em um sistema Prolog como o SWI-Prolog, e fazendo a consulta
?- solucao(S).
A resposta deverá aparecer após algum tempo. Dependendo do computador, isso pode demorar alguns minutos, o que é ruim. Além disso, são retornadas soluções equivalentes, em que apenas a ordem dos indivíduos muda. Isso tudo demonstra que o sistema Prolog está buscando em um espaço de estados muito maior do que necessário. Podemos fazer isso de maneira mais eficiente.
Não afeta a resolução dizer que o primeiro indivíduo na solução tem nome Ana, o segundo Ester, o terceiro Pablo e o quarto Rodolfo. Isso restringe o espaço de busca sem eliminar a solução. Se gerarmos as possibilidades já prendendo esses atributos, a busca pela solução se torna muito mais rápida. Definimos um novo predicado para gerar as possibilidades mais eficientemente:
gera_ef(P1, P2, P3, P4) :-
P1 = p(ana, _, _, _),
P2 = p(ester, _, _, _),
P3 = p(pablo, _, _, _),
P4 = p(rodolfo, _, _, _),
gera(P1), gera(P2), gera(P3), gera(P4),
todas_dif(P1, P2, P3, P4).
Depois é só usar esse predicado na busca pela solução. Além disso, só precisamos de uma solução, então vamos incluir um corte no final para evitar cálculos inúteis. O resultado é
solucao2(S) :-
S = [P1, P2, P3, P4],
gera_ef(P1, P2, P3, P4),
member(p(ana, _, aranha, _), S),
member(p(N, salgado, _, irmao), S), N \= ana,
member(p(_, matoso, almofada, _), S),
member(p(rodolfo, _, B, tia), S), B \= almofada,
member(p(_, levis, mosca, _), S),
member(p(ester, _, _, pai), S),
!.
E agora uma consulta do tipo
?- solucao2(S).
Vai achar a solução muito mais rápido. O código completo pode ser visto aqui.
P.S.: Agora que o github abriu o código para detecção de linguagens e syntax highlighting, queria ter tempo de implementar suporte decente a Prolog. O github acha que meus arquivos Prolog (extensão .pl) são Perl.
May 16th
O post abaixo foi publicado inicialmente em uma lista de discussão, em resposta ao email de outro participante, que estava em dúvida se a resposta do gabarito de uma questão do POSCOMP 2010 estava correta. A questão está na imagem abaixo, com a resposta do gabarito indicada em vermelho.
A questão aí é mais sutil do que parece à primeira vista. O enunciado não fala diretamente sobre como decidir se P != NP é verdade ou não, mas pede para considerar a hipótese P != NP como um problema de decisão a ser resolvido por um algoritmo, ou seja, por algum autômato. Um problema de decisão é uma questão para a qual um algoritmo deve responder apenas uma das duas possibilidades: ou SIM ou NÃO.
Normalmente, problemas de decisão são esquemas gerais para uma coleção de problemas particulares que são instâncias do caso geral; por exemplo, veja o problema da parada: decidir se uma máquina de Turing (ou seja, um programa) específica sempre termina sua computação com uma resposta definitiva, ou não. Esse é um problema geral. Uma instância desse problema seria pegar uma máquina de Turing M1 (que basicamente contém um programa específico) e perguntar: essa máquina M1 sempre pára e chega a uma resposta? Veja que para alguns casos a decisão é bem simples; imagine por exemplo uma máquina de Turing que desconsidera a entrada e apenas imprime uma sequência qualquer de símbolos na fita (digamos, a sequência “Hello, world!”). Essa máquina de Turing obviamente sempre pára e dá uma resposta. Seria fácil fazer uma algoritmo que, sempre que vê uma máquina de Turing que apenas imprime na fita (ou, digamos, um programa C que só contém printfs), responde SIM ao problema da parada, ou seja, essa máquina (ou programa) sempre vai parar.
A dificuldade do problema da parada não está em instâncias específicas, mas sim no problema geral: ter um algoritmo que consiga decidir isso para qualquer instância, para qualquer máquina de Turing, para qualquer programa de computador.
Mudando a discussão de indecibilidade para P e NP: se pensarmos no problema SAT, o problema de satisfatibilidade de fórmulas proposicionais, é fácil fazer um programa que responde a algumas dessas instâncias em tempo polinomial (por exemplo, fórmulas que só tem uma proposição, sem conectivos), mesmo que o problema geral seja NP (porque algumas instâncias nunca poderão ser resolvidas em tempo polinomial, se P != NP).
Agora vamos pensar em P != NP como um problema de decisão para ser resolvido por um algoritmo. O importante é que esse problema não tem instâncias. Ou P != NP é verdade, ou não é. É um problema único, e não um conjunto de instâncias de problemas, como os exemplos anteriores.
Com isso, vamos observar a afirmação IV: imagine que um algoritmo é “responda SIM” (ou seja, apenas imprimir a resposta “SIM”) e o outro é “responda NÃO”. É claro que, para o problema P != NP, um dos dois vai acertar. Não sabemos ainda qual dos dois é o certo, mas ou P != NP, ou não é verdade que P != NP (e portanto P = NP). Então acho que é fácil ver que a afirmativa IV está correta. Agora, quanto à III: desses 2 algoritmos da afirmativa IV, um dos dois está correto (embora não saibamos qual). Mas eles são de tempo polinomial? Claro que sim, eles apenas respondem imediatamente SIM ou NÃO, sem nenhum processamento. Ou seja, existe um algoritmo que responde ao problema de decisão P != NP em tempo polinomial (na verdade tempo constante, mas todo algoritmo O(1) também é O(n) e por aí vai), e, embora nós não saibamos que algoritmo é esse, sabemos que é um dos dois acima. Então a alternativa III está correta também. I e II obviamente não podem estar corretos se IV está, e na verdade não estão, exatamente porque os algoritmos mencionados em IV são suficientes.
May 13th
I thought Haskell Curry (the logician) was the only case of someone having programming languages named after both his name and surname. Haskell is the lazy functional programming language everyone (well, a lot of people) know and love, but there’s also a less-known language called Curry. It is quite similar to Haskell (the language), but adds logic programming capabilities on top of the whole functional programming stuff.
But today I noticed that there is another case: Piet Mondrian (the painter). Mondrian was an attempt at creating a kind of Haskell for .NET, while Piet is an esoteric, artistic, bidimensional programming language (which I remembered now because of a discussion with the #horaextrajp guys).
Maybe there are other cases, but these two are the ones I know. Other cases are welcome in the comments.
May 12th
This post was published before in an older blog of mine, and was originally written as a post in a mailing list. It was a discussion about calculating truth tables and it turned to SAT and someone said it was a very difficult problem. But it’s not hard to solve, just hard to solve efficiently.
Sometimes people get the wrong impression after a Theory of Computation course. A “hard” problem there is not necessarily hard to solve with a program, just hard to solve well in realistic circumstances. Take SAT, for example, the boolean satisfiability problem for propositional logic. It’s a classic example of a NP-complete problem, whose naive solution — generate all possible combinations of truth-values for each atomic proposition and then calculate the value for the whole formula — is clearly exponential. But this naive solution is very easy to write. For example, in Haskell we get something like the following:
import Data.List ( nub )import Data.Maybe ( fromJust )
data Formula = Var String | And Formula Formula | Or Formula Formula | Not Formula
getVarsDup (Var v) = [v]getVarsDup (And f1 f2) = (getVarsDup f1) ++ (getVarsDup f2)getVarsDup (Or f1 f2) = (getVarsDup f1) ++ (getVarsDup f2)getVarsDup (Not f) = getVarsDup f
genValues n = genAux $ [replicate n True] where genAux (v:vs) = if not $ or $ v then v:vs else genAux ((next v) : v : vs) next (True : ts) = (False : ts) next (False : ts) = (True : (next ts))
solveFormula (Var v) env = fromJust $ lookup v envsolveFormula (And f1 f2) env = (solveFormula f1 env) && (solveFormula f2 env)solveFormula (Or f1 f2) env = (solveFormula f1 env) || (solveFormula f2 env)solveFormula (Not f) env = not $ solveFormula f env
allSolutions f = map (solveFormula f) (map (zip vars) vals) where vars = nub $ getVarsDup f vals = genValues (length vars)
-- solve the SAT problemsat f = or $ allSolutions f
-- pointfree!sat2 = or . allSolutions
The idea is the same as creating a truth-table for the formula and then looking for a T in the possible values for it. Function genValues generates all possible values for the atomic propositions, and this is fed into solveFormula. It would be very easy to alter the code to output the complete truth-table; I leave it as an exercise to the reader. Another standard exercise in logic textbooks is to prove that if a formula contains n atomic propositions, the truth-table will have 2^n lines. Thus, generating all combinations is easily seen to be exponential in time.
But the fact that it is hard for the computer to solve doesn’t mean that it’s hard for someone to write a (naive) program to solve it. In practical applications, like circuit design, we may need to solve SAT for thousands of variables, and the naive algorithm would not work. For this we must use a more clever technique, like Binary Decision Diagrams. They’re not that hard to code either; the book Expert F# includes an example of BDDs in F#.
Apr 25th
Algo que esqueci no post anterior foi dar algumas indicações para quem quiser ler mais sobre o lambda-cálculo. Há muito material sobre o assunto na rede, mas alguns são melhores que outros. E alguns livros ainda têm conteúdo inacessível digitalmente. Seguem minhas indicações:
Isso deve ser leitura suficiente para se divertir por algumas semanas.
Apr 18th
Hoje vi um post no blog da Caelum sobre o lambda-cálculo (ou cálculo lambda) e, aproveitando que estou ensinando programação funcional atualmente, achei interessante escrever algumas ideias com a intenção de complementar o que está lá.
O lambda cálculo é um formalismo lógico usado hoje em dia principalmente na teoria das linguagens de programação. Algumas aplicações do lambda cálculo nessa área incluem o projeto e análise de linguagens de programação e sistemas de tipos e a semântica das linguagens de programação. Curiosamente, alguns linguistas também utilizam o cálculo para estudar a semântica das linguagens naturais. Além disso, o cálculo também é usado em ramos da lógica pura e na teoria das funções recursivas (que pode ser considerada parte da teoria da computação). O cálculo é mais famoso pela grande influência que teve na definição do paradigma de programação funcional.
Originalmente, Alonzo Church criou o lambda-cálculo como uma ferramenta para estudar os fundamentos da matemática, coisa que estava na moda no começo do século XX. A ideia de Church era usar a noção de “processo” ou “transformação” (função) como essencial para fundamentar a matemática, ao invés da noção de conjunto de Cantor. O lambda cálculo não deu muito certo para isso na época, mas acabou sendo importante em outra questão do tempo: a busca pela definição formal do que vem a ser um procedimento efetivo. Em termos atuais, diríamos que essa busca tentava definir formalmente o que é “computação”.
(A ideia de usar o conceito de transformação como central na matemática retornou na segunda metade do século XX através da Teoria das Categorias, mas isso é outra história.)
Com relação ao cálculo em si, sua versão original (chamada hoje de lambda-cálculo não-tipado) é muito simples. Essencialmente o cálculo é formado por expressões-lambda e duas operações de transformação. As expressões-lambda são sequências de símbolos formadas em uma sintaxe específica, como expressões da lógica proposicional ou de uma linguagem de programação. As operações transformam uma ou mais expressões já existentes em uma nova expressão.
A sintaxe das expressões-lambda é determinada pelas duas operações: abstração e aplicação (sendo que a aplicação envolve uma operação de substituição chamada conversão-β). Uma expressão-lambda pode ser uma variável, uma abstração de uma expressão, ou uma aplicação de duas expressões:
A sintaxe é apenas isso. Formalmente, é preciso definir essa sintaxe mais cuidadosamente para garantir que uma expressão-lambda sempre pode ser analisada de maneira não-ambígua, mas para os propósitos deste post isso é suficiente. Alguns exemplos de expressões-lambda:
Agora vamos ao significado dessas coisas: a ideia é que abstração signifique a criação de uma função, e aplicação signfique o uso ou chamada dessa função em cima de um parâmetro. A conversão-β é a regra de substituição que diz como a aplicação deve funcionar:
Por exemplo: (λx.x) y tem como resultado a expressão y (E = x, F = y); (λx.x)(λx.y) tem como resultado (λx.y) (E = x, F = (λx.y)). E é só isso, substituição textual. No lambda-cálculo puro só podemos criar expressões como definido acima. Mas vamos incluir algumas definições adicionais como a função infixa de adição e números naturais como 2 e 3 e podemos ter expressões como (λx.x + 2) 3, que se for avaliada pela conversão-β tem como resultado 3 + 2, exatamente o que esperamos da aplicação de funções. (Indo mais longe no estudo do cálculo, vemos que é possível definir os próprios números naturais e a operação de adição no lambda-cálculo puro, mas não vamos passear por aí neste post; vide a entrada na wikipedia pra numerais de Church).
A definição do lambda-calculus vista acima não inclui funções de dois parâmetros, mas isso não é realmente necessário. Uma função de dois argumentos pode ser criada simplesmente fazendo duas abstrações em sequência: uma função usando a adição poderia ser definida como λx.λy.x+y. Para aplicar essa função também temos que usar duas aplicações: ((λx.λy.x+y) 3) 2 se transforma em (λy.3+y) 2, que se transforma em 3+2. Esse processo de considerar uma função de dois argumentos como uma sequência de duas funções é o tal currying muito usado em linguagens funcionais.
Após essa conversa toda, chegamos ao ponto em que dá para explicar em mais detalhes a definição de valores-verdade no cálculo puro. Podemos definir os valores true e false como:
A questão é: por que definir true e false dessa forma? A definição é, em boa medida, arbitrária. Outras definições para os valores-verdade são possíveis, mas essas acima são simples e funcionam. Para ver que funcionam, basta pensar em como definir uma expressão condicional (if-then-else) no cálculo. O requerimento básico é:
Para implementar isso no cálculo, dadas as definições de true e false acima, basta usar aplicação diretamente. Dada uma expressão-lambda E com resultado igual a um valor-verdade,
É fácil ver que isso funciona:
As outras definições (de pares, and, or, not, e mesmo as codificações de números e aritmética no cálculo puro) seguem esse padrão. As definições em si são arbitrárias, mas o importante é que elas funcionem em um contexto maior.
O cálculo tem outras coisas muito interessantes além disso. O cálculo não-tipado visto acima, que é basicamente um modelo de substituição, é muito usado para estudar sistemas de macros em linguagens da família Lisp, por exemplo. Uma outra dimensão interessante é ir para os cálculos tipados e ver como os sistemas de tipos das linguagens de programação podem ser formalizados (existe um livro inteiro sobre isso). Os combinadores da lógica combinatória de Haskell Curry, um sistema formal muito relacionado ao lambda-cálculo, também apresentam algumas ideias dignas de investigação, como programação point-free (ou tácita), a definição de bibliotecas de combinadores, e as técnicas para implementação de linguagens funcionais lazy.
E, se você estudar e entender tudo isso, talvez você consiga entender a linguagem Scala na sua totalidade
Recent Comments